diff --git a/apps/docs/content/docs/de/triggers/schedule.mdx b/apps/docs/content/docs/de/triggers/schedule.mdx index 4fdb02fc18..f9bcf7c12d 100644 --- a/apps/docs/content/docs/de/triggers/schedule.mdx +++ b/apps/docs/content/docs/de/triggers/schedule.mdx @@ -56,7 +56,7 @@ Sie müssen Ihren Workflow bereitstellen, damit der Zeitplan mit der Ausführung ## Automatische Deaktivierung -Zeitpläne werden nach **10 aufeinanderfolgenden Fehlschlägen** automatisch deaktiviert, um unkontrollierte Fehler zu verhindern. Bei Deaktivierung: +Zeitpläne werden nach **100 aufeinanderfolgenden Fehlschlägen** automatisch deaktiviert, um unkontrollierte Fehler zu verhindern. Bei Deaktivierung: - Erscheint ein Warnhinweis auf dem Zeitplan-Block - Die Ausführung des Zeitplans wird gestoppt diff --git a/apps/docs/content/docs/en/triggers/schedule.mdx b/apps/docs/content/docs/en/triggers/schedule.mdx index bb7bfbaa80..ec2f65e91e 100644 --- a/apps/docs/content/docs/en/triggers/schedule.mdx +++ b/apps/docs/content/docs/en/triggers/schedule.mdx @@ -56,7 +56,7 @@ You must deploy your workflow for the schedule to start running. Configure the s ## Automatic Disabling -Schedules automatically disable after **10 consecutive failures** to prevent runaway errors. When disabled: +Schedules automatically disable after **100 consecutive failures** to prevent runaway errors. When disabled: - A warning badge appears on the schedule block - The schedule stops executing diff --git a/apps/docs/content/docs/es/triggers/schedule.mdx b/apps/docs/content/docs/es/triggers/schedule.mdx index 636e87b19d..d41646be47 100644 --- a/apps/docs/content/docs/es/triggers/schedule.mdx +++ b/apps/docs/content/docs/es/triggers/schedule.mdx @@ -56,7 +56,7 @@ Debes desplegar tu flujo de trabajo para que la programación comience a ejecuta ## Desactivación automática -Las programaciones se desactivan automáticamente después de **10 fallos consecutivos** para evitar errores descontrolados. Cuando se desactiva: +Las programaciones se desactivan automáticamente después de **100 fallos consecutivos** para evitar errores descontrolados. Cuando se desactiva: - Aparece una insignia de advertencia en el bloque de programación - La programación deja de ejecutarse diff --git a/apps/docs/content/docs/fr/triggers/schedule.mdx b/apps/docs/content/docs/fr/triggers/schedule.mdx index df9e112687..d32357afe3 100644 --- a/apps/docs/content/docs/fr/triggers/schedule.mdx +++ b/apps/docs/content/docs/fr/triggers/schedule.mdx @@ -56,7 +56,7 @@ Vous devez déployer votre workflow pour que la planification commence à s'exé ## Désactivation automatique -Les planifications se désactivent automatiquement après **10 échecs consécutifs** pour éviter les erreurs incontrôlées. Lorsqu'elle est désactivée : +Les planifications se désactivent automatiquement après **100 échecs consécutifs** pour éviter les erreurs incontrôlées. Lorsqu'elle est désactivée : - Un badge d'avertissement apparaît sur le bloc de planification - La planification cesse de s'exécuter diff --git a/apps/docs/content/docs/ja/triggers/schedule.mdx b/apps/docs/content/docs/ja/triggers/schedule.mdx index f88d45d937..efb0a38111 100644 --- a/apps/docs/content/docs/ja/triggers/schedule.mdx +++ b/apps/docs/content/docs/ja/triggers/schedule.mdx @@ -56,7 +56,7 @@ import { Image } from '@/components/ui/image' ## 自動無効化 -スケジュールは**10回連続で失敗**すると、エラーの連鎖を防ぐため自動的に無効化されます。無効化されると: +スケジュールは**100回連続で失敗**すると、エラーの連鎖を防ぐため自動的に無効化されます。無効化されると: - スケジュールブロックに警告バッジが表示されます - スケジュールの実行が停止します diff --git a/apps/docs/content/docs/zh/triggers/schedule.mdx b/apps/docs/content/docs/zh/triggers/schedule.mdx index 84d7d2f39e..ca9d0febad 100644 --- a/apps/docs/content/docs/zh/triggers/schedule.mdx +++ b/apps/docs/content/docs/zh/triggers/schedule.mdx @@ -56,7 +56,7 @@ import { Image } from '@/components/ui/image' ## 自动禁用 -计划在连续 **10 次失败** 后会自动禁用,以防止错误持续发生。禁用后: +计划在连续 **100 次失败** 后会自动禁用,以防止错误持续发生。禁用后: - 计划块上会显示警告徽章 - 计划将停止执行 diff --git a/apps/sim/app/api/schedules/route.test.ts b/apps/sim/app/api/schedules/route.test.ts index ac1ece178d..776b6be3cf 100644 --- a/apps/sim/app/api/schedules/route.test.ts +++ b/apps/sim/app/api/schedules/route.test.ts @@ -144,7 +144,7 @@ describe('Schedule GET API', () => { it('indicates disabled schedule with failures', async () => { mockDbChain([ [{ userId: 'user-1', workspaceId: null }], - [{ id: 'sched-1', status: 'disabled', failedCount: 10 }], + [{ id: 'sched-1', status: 'disabled', failedCount: 100 }], ]) const res = await GET(createRequest('http://test/api/schedules?workflowId=wf-1')) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/schedule-info/schedule-info.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/schedule-info/schedule-info.tsx index 5c2f5e487a..dc0a758536 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/schedule-info/schedule-info.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/schedule-info/schedule-info.tsx @@ -1,11 +1,9 @@ -import { useCallback, useEffect, useState } from 'react' -import { AlertTriangle } from 'lucide-react' import { useParams } from 'next/navigation' -import { createLogger } from '@/lib/logs/console/logger' +import { Badge } from '@/components/emcn' import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils' +import { useRedeployWorkflowSchedule, useScheduleQuery } from '@/hooks/queries/schedules' import { useSubBlockStore } from '@/stores/workflows/subblock/store' - -const logger = createLogger('ScheduleStatus') +import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants' interface ScheduleInfoProps { blockId: string @@ -20,172 +18,93 @@ interface ScheduleInfoProps { export function ScheduleInfo({ blockId, isPreview = false }: ScheduleInfoProps) { const params = useParams() const workflowId = params.workflowId as string - const [scheduleStatus, setScheduleStatus] = useState<'active' | 'disabled' | null>(null) - const [nextRunAt, setNextRunAt] = useState(null) - const [lastRanAt, setLastRanAt] = useState(null) - const [failedCount, setFailedCount] = useState(0) - const [isLoadingStatus, setIsLoadingStatus] = useState(true) - const [savedCronExpression, setSavedCronExpression] = useState(null) - const [isRedeploying, setIsRedeploying] = useState(false) - const [hasSchedule, setHasSchedule] = useState(false) const scheduleTimezone = useSubBlockStore((state) => state.getValue(blockId, 'timezone')) - const fetchScheduleStatus = useCallback(async () => { - if (isPreview) return - - setIsLoadingStatus(true) - try { - const response = await fetch(`/api/schedules?workflowId=${workflowId}&blockId=${blockId}`) - if (response.ok) { - const data = await response.json() - if (data.schedule) { - setHasSchedule(true) - setScheduleStatus(data.schedule.status) - setNextRunAt(data.schedule.nextRunAt ? new Date(data.schedule.nextRunAt) : null) - setLastRanAt(data.schedule.lastRanAt ? new Date(data.schedule.lastRanAt) : null) - setFailedCount(data.schedule.failedCount || 0) - setSavedCronExpression(data.schedule.cronExpression || null) - } else { - // No schedule exists (workflow not deployed or no schedule block) - setHasSchedule(false) - setScheduleStatus(null) - setNextRunAt(null) - setLastRanAt(null) - setFailedCount(0) - setSavedCronExpression(null) - } - } - } catch (error) { - logger.error('Error fetching schedule status', { error }) - } finally { - setIsLoadingStatus(false) - } - }, [workflowId, blockId, isPreview]) - - useEffect(() => { - if (!isPreview) { - fetchScheduleStatus() - } - }, [isPreview, fetchScheduleStatus]) + const { data: schedule, isLoading } = useScheduleQuery(workflowId, blockId, { + enabled: !isPreview, + }) - /** - * Handles redeploying the workflow when schedule is disabled due to failures. - * Redeploying will recreate the schedule with reset failure count. - */ - const handleRedeploy = async () => { - if (isPreview || isRedeploying) return + const redeployMutation = useRedeployWorkflowSchedule() - setIsRedeploying(true) - try { - const response = await fetch(`/api/workflows/${workflowId}/deploy`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ deployChatEnabled: false }), - }) - - if (response.ok) { - // Refresh schedule status after redeploy - await fetchScheduleStatus() - logger.info('Workflow redeployed successfully to reset schedule', { workflowId, blockId }) - } else { - const errorData = await response.json() - logger.error('Failed to redeploy workflow', { error: errorData.error }) - } - } catch (error) { - logger.error('Error redeploying workflow', { error }) - } finally { - setIsRedeploying(false) - } + const handleRedeploy = () => { + if (isPreview || redeployMutation.isPending) return + redeployMutation.mutate({ workflowId, blockId }) } - // Don't render anything if there's no deployed schedule - if (!hasSchedule && !isLoadingStatus) { + if (!schedule || isLoading) { return null } + const timezone = scheduleTimezone || schedule?.timezone || 'UTC' + const failedCount = schedule?.failedCount || 0 + const isDisabled = schedule?.status === 'disabled' + const nextRunAt = schedule?.nextRunAt ? new Date(schedule.nextRunAt) : null + return ( -
- {isLoadingStatus ? ( -
-
- Loading schedule status... -
- ) : ( +
+ {/* Status badges */} + {(failedCount > 0 || isDisabled) && (
- {/* Failure badge with redeploy action */} - {failedCount >= 10 && scheduleStatus === 'disabled' && ( - - )} - - {/* Show warning for failed runs under threshold */} - {failedCount > 0 && failedCount < 10 && ( -
- - ⚠️ {failedCount} failed run{failedCount !== 1 ? 's' : ''} - -
- )} - - {/* Cron expression human-readable description */} - {savedCronExpression && ( -

- Runs{' '} - {parseCronToHumanReadable( - savedCronExpression, - scheduleTimezone || 'UTC' - ).toLowerCase()} +

+ {failedCount >= MAX_CONSECUTIVE_FAILURES && isDisabled ? ( + + {redeployMutation.isPending ? 'redeploying...' : 'disabled'} + + ) : failedCount > 0 ? ( + + {failedCount} failed + + ) : null} +
+ {failedCount >= MAX_CONSECUTIVE_FAILURES && isDisabled && ( +

+ Disabled after {MAX_CONSECUTIVE_FAILURES} consecutive failures

)} - - {/* Next run time */} - {nextRunAt && ( -

- Next run:{' '} - {nextRunAt.toLocaleString('en-US', { - timeZone: scheduleTimezone || 'UTC', - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true, - })}{' '} - {scheduleTimezone || 'UTC'} + {redeployMutation.isError && ( +

+ Failed to redeploy. Please try again.

)} +
+ )} - {/* Last ran time */} - {lastRanAt && ( -

- Last ran:{' '} - {lastRanAt.toLocaleString('en-US', { - timeZone: scheduleTimezone || 'UTC', - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true, - })}{' '} - {scheduleTimezone || 'UTC'} -

+ {/* Schedule info - only show when active */} + {!isDisabled && ( +
+ {schedule?.cronExpression && ( + {parseCronToHumanReadable(schedule.cronExpression, timezone)} + )} + {nextRunAt && ( + <> + {schedule?.cronExpression && ·} + + Next:{' '} + {nextRunAt.toLocaleString('en-US', { + timeZone: timezone, + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true, + })} + + )}
)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx index ab9f43f080..2dca2f0fe6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx @@ -8,7 +8,6 @@ import { ModalHeader, } from '@/components/emcn/components' import { Trash } from '@/components/emcn/icons/trash' -import { Alert, AlertDescription } from '@/components/ui/alert' import { cn } from '@/lib/core/utils/cn' import { createLogger } from '@/lib/logs/console/logger' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' @@ -367,12 +366,7 @@ export function TriggerSave({ saveStatus === 'error' && 'bg-red-600 hover:bg-red-700' )} > - {saveStatus === 'saving' && ( - <> -
- Saving... - - )} + {saveStatus === 'saving' && 'Saving...'} {saveStatus === 'saved' && 'Saved'} {saveStatus === 'error' && 'Error'} {saveStatus === 'idle' && (webhookId ? 'Update Configuration' : 'Save Configuration')} @@ -394,59 +388,48 @@ export function TriggerSave({ )}
- {errorMessage && ( - - {errorMessage} - - )} + {errorMessage &&

{errorMessage}

} {webhookId && hasWebhookUrlDisplay && ( -
+
- Test Webhook URL + + Test Webhook URL +
{testUrl ? ( - + <> + + {testUrlExpiresAt && ( +

+ Expires {new Date(testUrlExpiresAt).toLocaleString()} +

+ )} + ) : ( -

- Generate a temporary URL that executes this webhook against the live (undeployed) - workflow state. -

- )} - {testUrlExpiresAt && ( -

- Expires at {new Date(testUrlExpiresAt).toLocaleString()} +

+ Generate a temporary URL to test against the live (undeployed) workflow state.

)}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-schedule-info.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-schedule-info.ts index 0d740344d6..591c31816d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-schedule-info.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-schedule-info.ts @@ -1,10 +1,10 @@ -import { useCallback, useEffect, useState } from 'react' -import { createLogger } from '@/lib/logs/console/logger' -import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils' +import { useCallback } from 'react' +import { + useReactivateSchedule, + useScheduleInfo as useScheduleInfoQuery, +} from '@/hooks/queries/schedules' import type { ScheduleInfo } from '../types' -const logger = createLogger('useScheduleInfo') - /** * Return type for the useScheduleInfo hook */ @@ -18,7 +18,7 @@ export interface UseScheduleInfoReturn { } /** - * Custom hook for fetching schedule information + * Custom hook for fetching schedule information using TanStack Query * * @param blockId - The ID of the block * @param blockType - The type of the block @@ -30,96 +30,37 @@ export function useScheduleInfo( blockType: string, workflowId: string ): UseScheduleInfoReturn { - const [isLoading, setIsLoading] = useState(false) - const [scheduleInfo, setScheduleInfo] = useState(null) - - const fetchScheduleInfo = useCallback( - async (wfId: string) => { - if (!wfId) return - - try { - setIsLoading(true) - - const params = new URLSearchParams({ - workflowId: wfId, - blockId, - }) - - const response = await fetch(`/api/schedules?${params}`, { - cache: 'no-store', - headers: { 'Cache-Control': 'no-cache' }, - }) - - if (!response.ok) { - setScheduleInfo(null) - return - } - - const data = await response.json() - - if (!data.schedule) { - setScheduleInfo(null) - return - } - - const schedule = data.schedule - const scheduleTimezone = schedule.timezone || 'UTC' - - setScheduleInfo({ - scheduleTiming: schedule.cronExpression - ? parseCronToHumanReadable(schedule.cronExpression, scheduleTimezone) - : 'Unknown schedule', - nextRunAt: schedule.nextRunAt, - lastRanAt: schedule.lastRanAt, - timezone: scheduleTimezone, - status: schedule.status, - isDisabled: schedule.status === 'disabled', - failedCount: schedule.failedCount || 0, - id: schedule.id, - }) - } catch (error) { - logger.error('Error fetching schedule info:', error) - setScheduleInfo(null) - } finally { - setIsLoading(false) - } - }, - [blockId] + const { scheduleInfo: queryScheduleInfo, isLoading } = useScheduleInfoQuery( + workflowId, + blockId, + blockType ) + const reactivateMutation = useReactivateSchedule() + const reactivateSchedule = useCallback( async (scheduleId: string) => { - try { - const response = await fetch(`/api/schedules/${scheduleId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action: 'reactivate' }), - }) - - if (response.ok && workflowId) { - await fetchScheduleInfo(workflowId) - } else { - logger.error('Failed to reactivate schedule') - } - } catch (error) { - logger.error('Error reactivating schedule:', error) - } + await reactivateMutation.mutateAsync({ + scheduleId, + workflowId, + blockId, + }) }, - [workflowId, fetchScheduleInfo] + [reactivateMutation, workflowId, blockId] ) - useEffect(() => { - if (blockType === 'schedule' && workflowId) { - fetchScheduleInfo(workflowId) - } else { - setScheduleInfo(null) - setIsLoading(false) - } - - return () => { - setIsLoading(false) - } - }, [blockType, workflowId, fetchScheduleInfo]) + const scheduleInfo: ScheduleInfo | null = queryScheduleInfo + ? { + scheduleTiming: queryScheduleInfo.scheduleTiming, + nextRunAt: queryScheduleInfo.nextRunAt, + lastRanAt: queryScheduleInfo.lastRanAt, + timezone: queryScheduleInfo.timezone, + status: queryScheduleInfo.status, + isDisabled: queryScheduleInfo.isDisabled, + failedCount: queryScheduleInfo.failedCount, + id: queryScheduleInfo.id, + } + : null return { scheduleInfo, diff --git a/apps/sim/background/schedule-execution.ts b/apps/sim/background/schedule-execution.ts index 2d8f618f50..f5a7657c41 100644 --- a/apps/sim/background/schedule-execution.ts +++ b/apps/sim/background/schedule-execution.ts @@ -27,11 +27,10 @@ import { type ExecutionMetadata, ExecutionSnapshot } from '@/executor/execution/ import type { ExecutionResult } from '@/executor/types' import { createEnvVarPattern } from '@/executor/utils/reference-validation' import { mergeSubblockState } from '@/stores/workflows/server-utils' +import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants' const logger = createLogger('TriggerScheduleExecution') -const MAX_CONSECUTIVE_FAILURES = 10 - type WorkflowRecord = typeof workflow.$inferSelect type WorkflowScheduleUpdate = Partial type ExecutionCoreResult = Awaited> diff --git a/apps/sim/hooks/queries/schedules.ts b/apps/sim/hooks/queries/schedules.ts new file mode 100644 index 0000000000..19116439ac --- /dev/null +++ b/apps/sim/hooks/queries/schedules.ts @@ -0,0 +1,184 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { createLogger } from '@/lib/logs/console/logger' +import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils' + +const logger = createLogger('ScheduleQueries') + +export const scheduleKeys = { + all: ['schedules'] as const, + schedule: (workflowId: string, blockId: string) => + [...scheduleKeys.all, workflowId, blockId] as const, +} + +export interface ScheduleData { + id: string + status: 'active' | 'disabled' + cronExpression: string | null + nextRunAt: string | null + lastRanAt: string | null + timezone: string + failedCount: number +} + +export interface ScheduleInfo { + id: string + status: 'active' | 'disabled' + scheduleTiming: string + nextRunAt: string | null + lastRanAt: string | null + timezone: string + isDisabled: boolean + failedCount: number +} + +/** + * Fetches schedule data for a specific workflow block + */ +async function fetchSchedule(workflowId: string, blockId: string): Promise { + const params = new URLSearchParams({ workflowId, blockId }) + const response = await fetch(`/api/schedules?${params}`, { + cache: 'no-store', + headers: { 'Cache-Control': 'no-cache' }, + }) + + if (!response.ok) { + return null + } + + const data = await response.json() + return data.schedule || null +} + +/** + * Hook to fetch schedule data for a workflow block + */ +export function useScheduleQuery( + workflowId: string | undefined, + blockId: string | undefined, + options?: { enabled?: boolean } +) { + return useQuery({ + queryKey: scheduleKeys.schedule(workflowId ?? '', blockId ?? ''), + queryFn: () => fetchSchedule(workflowId!, blockId!), + enabled: !!workflowId && !!blockId && (options?.enabled ?? true), + staleTime: 30 * 1000, // 30 seconds + retry: false, + }) +} + +/** + * Hook to get processed schedule info with human-readable timing + */ +export function useScheduleInfo( + workflowId: string | undefined, + blockId: string | undefined, + blockType: string, + options?: { timezone?: string } +): { + scheduleInfo: ScheduleInfo | null + isLoading: boolean + refetch: () => void +} { + const isScheduleBlock = blockType === 'schedule' + + const { data, isLoading, refetch } = useScheduleQuery(workflowId, blockId, { + enabled: isScheduleBlock, + }) + + if (!data) { + return { scheduleInfo: null, isLoading, refetch } + } + + const timezone = options?.timezone || data.timezone || 'UTC' + const scheduleTiming = data.cronExpression + ? parseCronToHumanReadable(data.cronExpression, timezone) + : 'Unknown schedule' + + return { + scheduleInfo: { + id: data.id, + status: data.status, + scheduleTiming, + nextRunAt: data.nextRunAt, + lastRanAt: data.lastRanAt, + timezone, + isDisabled: data.status === 'disabled', + failedCount: data.failedCount || 0, + }, + isLoading, + refetch, + } +} + +/** + * Mutation to reactivate a disabled schedule + */ +export function useReactivateSchedule() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ + scheduleId, + workflowId, + blockId, + }: { + scheduleId: string + workflowId: string + blockId: string + }) => { + const response = await fetch(`/api/schedules/${scheduleId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'reactivate' }), + }) + + if (!response.ok) { + throw new Error('Failed to reactivate schedule') + } + + return { workflowId, blockId } + }, + onSuccess: ({ workflowId, blockId }) => { + logger.info('Schedule reactivated', { workflowId, blockId }) + queryClient.invalidateQueries({ + queryKey: scheduleKeys.schedule(workflowId, blockId), + }) + }, + onError: (error) => { + logger.error('Failed to reactivate schedule', { error }) + }, + }) +} + +/** + * Mutation to redeploy a workflow (which recreates the schedule) + */ +export function useRedeployWorkflowSchedule() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ workflowId, blockId }: { workflowId: string; blockId: string }) => { + const response = await fetch(`/api/workflows/${workflowId}/deploy`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ deployChatEnabled: false }), + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'Failed to redeploy workflow') + } + + return { workflowId, blockId } + }, + onSuccess: ({ workflowId, blockId }) => { + logger.info('Workflow redeployed for schedule reset', { workflowId, blockId }) + queryClient.invalidateQueries({ + queryKey: scheduleKeys.schedule(workflowId, blockId), + }) + }, + onError: (error) => { + logger.error('Failed to redeploy workflow', { error }) + }, + }) +} diff --git a/apps/sim/lib/webhooks/gmail-polling-service.ts b/apps/sim/lib/webhooks/gmail-polling-service.ts index ff1872ce77..6c406ba1e7 100644 --- a/apps/sim/lib/webhooks/gmail-polling-service.ts +++ b/apps/sim/lib/webhooks/gmail-polling-service.ts @@ -8,11 +8,10 @@ import { createLogger } from '@/lib/logs/console/logger' import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' import type { GmailAttachment } from '@/tools/gmail/types' import { downloadAttachments, extractAttachmentInfo } from '@/tools/gmail/utils' +import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants' const logger = createLogger('GmailPollingService') -const MAX_CONSECUTIVE_FAILURES = 10 - interface GmailWebhookConfig { labelIds: string[] labelFilterBehavior: 'INCLUDE' | 'EXCLUDE' diff --git a/apps/sim/lib/webhooks/outlook-polling-service.ts b/apps/sim/lib/webhooks/outlook-polling-service.ts index 0ae291a699..2ff8c9a9bb 100644 --- a/apps/sim/lib/webhooks/outlook-polling-service.ts +++ b/apps/sim/lib/webhooks/outlook-polling-service.ts @@ -7,11 +7,10 @@ import { pollingIdempotency } from '@/lib/core/idempotency' import { getBaseUrl } from '@/lib/core/utils/urls' import { createLogger } from '@/lib/logs/console/logger' import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' +import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants' const logger = createLogger('OutlookPollingService') -const MAX_CONSECUTIVE_FAILURES = 10 - async function markWebhookFailed(webhookId: string) { try { const result = await db diff --git a/apps/sim/lib/webhooks/rss-polling-service.ts b/apps/sim/lib/webhooks/rss-polling-service.ts index 1cf8c2b5a5..8c81d80c59 100644 --- a/apps/sim/lib/webhooks/rss-polling-service.ts +++ b/apps/sim/lib/webhooks/rss-polling-service.ts @@ -7,10 +7,9 @@ import { pollingIdempotency } from '@/lib/core/idempotency/service' import { createPinnedUrl, validateUrlWithDNS } from '@/lib/core/security/input-validation' import { getBaseUrl } from '@/lib/core/utils/urls' import { createLogger } from '@/lib/logs/console/logger' +import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants' const logger = createLogger('RssPollingService') - -const MAX_CONSECUTIVE_FAILURES = 10 const MAX_GUIDS_TO_TRACK = 100 // Track recent guids to prevent duplicates interface RssWebhookConfig { diff --git a/apps/sim/triggers/constants.ts b/apps/sim/triggers/constants.ts index 3c47d671f5..d731246248 100644 --- a/apps/sim/triggers/constants.ts +++ b/apps/sim/triggers/constants.ts @@ -40,3 +40,9 @@ export const TRIGGER_RUNTIME_SUBBLOCK_IDS: string[] = [ 'testUrl', 'testUrlExpiresAt', ] + +/** + * Maximum number of consecutive failures before a trigger (schedule/webhook) is auto-disabled. + * This prevents runaway errors from continuously executing failing workflows. + */ +export const MAX_CONSECUTIVE_FAILURES = 100