improvement(schedules): use tanstack query to fetch schedule data, cleanup ui on schedule info component (#2584)

* improvement(schedules): use tanstack query to fetch schedule data, cleanup ui on schedule info component

* update trigger-save UI, increase auto disable to 100 consecutive from 10

* updated docs

* consolidate consts
This commit is contained in:
Waleed
2025-12-25 12:09:58 -08:00
committed by GitHub
parent d79696beae
commit 3201abab56
16 changed files with 337 additions and 308 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -56,7 +56,7 @@ import { Image } from '@/components/ui/image'
## 自動無効化
スケジュールは**10回連続で失敗**すると、エラーの連鎖を防ぐため自動的に無効化されます。無効化されると:
スケジュールは**100回連続で失敗**すると、エラーの連鎖を防ぐため自動的に無効化されます。無効化されると:
- スケジュールブロックに警告バッジが表示されます
- スケジュールの実行が停止します

View File

@@ -56,7 +56,7 @@ import { Image } from '@/components/ui/image'
## 自动禁用
计划在连续 **10 次失败** 后会自动禁用,以防止错误持续发生。禁用后:
计划在连续 **100 次失败** 后会自动禁用,以防止错误持续发生。禁用后:
- 计划块上会显示警告徽章
- 计划将停止执行

View File

@@ -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'))

View File

@@ -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<Date | null>(null)
const [lastRanAt, setLastRanAt] = useState<Date | null>(null)
const [failedCount, setFailedCount] = useState<number>(0)
const [isLoadingStatus, setIsLoadingStatus] = useState(true)
const [savedCronExpression, setSavedCronExpression] = useState<string | null>(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
const { data: schedule, isLoading } = useScheduleQuery(workflowId, blockId, {
enabled: !isPreview,
})
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])
const redeployMutation = useRedeployWorkflowSchedule()
useEffect(() => {
if (!isPreview) {
fetchScheduleStatus()
}
}, [isPreview, fetchScheduleStatus])
/**
* 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
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 (
<div className='mt-2'>
{isLoadingStatus ? (
<div className='flex items-center gap-2 text-muted-foreground text-sm'>
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
Loading schedule status...
</div>
) : (
<div className='space-y-1.5'>
{/* Status badges */}
{(failedCount > 0 || isDisabled) && (
<div className='space-y-1'>
{/* Failure badge with redeploy action */}
{failedCount >= 10 && scheduleStatus === 'disabled' && (
<button
type='button'
onClick={handleRedeploy}
disabled={isRedeploying}
className='flex w-full cursor-pointer items-center gap-2 rounded-md bg-destructive/10 px-3 py-2 text-left text-destructive text-sm transition-colors hover:bg-destructive/20 disabled:cursor-not-allowed disabled:opacity-50'
>
{isRedeploying ? (
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
) : (
<AlertTriangle className='h-4 w-4 flex-shrink-0' />
)}
<span>
{isRedeploying
? 'Redeploying...'
: `Schedule disabled after ${failedCount} failures - Click to redeploy`}
</span>
</button>
)}
{/* Show warning for failed runs under threshold */}
{failedCount > 0 && failedCount < 10 && (
<div className='flex items-center gap-2'>
<span className='text-destructive text-sm'>
{failedCount} failed run{failedCount !== 1 ? 's' : ''}
</span>
</div>
)}
{/* Cron expression human-readable description */}
{savedCronExpression && (
<p className='text-muted-foreground text-sm'>
Runs{' '}
{parseCronToHumanReadable(
savedCronExpression,
scheduleTimezone || 'UTC'
).toLowerCase()}
<div className='flex flex-wrap items-center gap-2'>
{failedCount >= MAX_CONSECUTIVE_FAILURES && isDisabled ? (
<Badge
variant='outline'
className='cursor-pointer'
style={{
borderColor: 'var(--warning)',
color: 'var(--warning)',
}}
onClick={handleRedeploy}
>
{redeployMutation.isPending ? 'redeploying...' : 'disabled'}
</Badge>
) : failedCount > 0 ? (
<Badge
variant='outline'
style={{
borderColor: 'var(--warning)',
color: 'var(--warning)',
}}
>
{failedCount} failed
</Badge>
) : null}
</div>
{failedCount >= MAX_CONSECUTIVE_FAILURES && isDisabled && (
<p className='text-[12px] text-[var(--text-tertiary)]'>
Disabled after {MAX_CONSECUTIVE_FAILURES} consecutive failures
</p>
)}
{redeployMutation.isError && (
<p className='text-[12px] text-[var(--text-error)]'>
Failed to redeploy. Please try again.
</p>
)}
</div>
)}
{/* Next run time */}
{/* Schedule info - only show when active */}
{!isDisabled && (
<div className='text-[12px] text-[var(--text-tertiary)]'>
{schedule?.cronExpression && (
<span>{parseCronToHumanReadable(schedule.cronExpression, timezone)}</span>
)}
{nextRunAt && (
<p className='text-sm'>
<span className='font-medium'>Next run:</span>{' '}
{nextRunAt.toLocaleString('en-US', {
timeZone: scheduleTimezone || 'UTC',
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
})}{' '}
{scheduleTimezone || 'UTC'}
</p>
)}
{/* Last ran time */}
{lastRanAt && (
<p className='text-muted-foreground text-sm'>
<span className='font-medium'>Last ran:</span>{' '}
{lastRanAt.toLocaleString('en-US', {
timeZone: scheduleTimezone || 'UTC',
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
})}{' '}
{scheduleTimezone || 'UTC'}
</p>
<>
{schedule?.cronExpression && <span className='mx-1'>·</span>}
<span>
Next:{' '}
{nextRunAt.toLocaleString('en-US', {
timeZone: timezone,
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
})}
</span>
</>
)}
</div>
)}

View File

@@ -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' && (
<>
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
Saving...
</>
)}
{saveStatus === 'saving' && 'Saving...'}
{saveStatus === 'saved' && 'Saved'}
{saveStatus === 'error' && 'Error'}
{saveStatus === 'idle' && (webhookId ? 'Update Configuration' : 'Save Configuration')}
@@ -394,59 +388,48 @@ export function TriggerSave({
)}
</div>
{errorMessage && (
<Alert variant='destructive' className='mt-2'>
<AlertDescription>{errorMessage}</AlertDescription>
</Alert>
)}
{errorMessage && <p className='mt-2 text-[12px] text-[var(--text-error)]'>{errorMessage}</p>}
{webhookId && hasWebhookUrlDisplay && (
<div className='mt-2 space-y-1'>
<div className='mt-4 space-y-2'>
<div className='flex items-center justify-between'>
<span className='font-medium text-sm'>Test Webhook URL</span>
<span className='font-medium text-[13px] text-[var(--text-primary)]'>
Test Webhook URL
</span>
<Button
variant='outline'
variant='ghost'
onClick={generateTestUrl}
disabled={isGeneratingTestUrl || isProcessing}
className='h-[32px] rounded-[8px] px-[12px]'
>
{isGeneratingTestUrl ? (
<>
<div className='mr-2 h-3 w-3 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
Generating
</>
) : testUrl ? (
'Regenerate'
) : (
'Generate'
)}
{isGeneratingTestUrl ? 'Generating…' : testUrl ? 'Regenerate' : 'Generate'}
</Button>
</div>
{testUrl ? (
<ShortInput
blockId={blockId}
subBlockId={`${subBlockId}-test-url`}
config={{
id: `${subBlockId}-test-url`,
type: 'short-input',
readOnly: true,
showCopyButton: true,
}}
value={testUrl}
readOnly={true}
showCopyButton={true}
disabled={isPreview || disabled}
isPreview={isPreview}
/>
<>
<ShortInput
blockId={blockId}
subBlockId={`${subBlockId}-test-url`}
config={{
id: `${subBlockId}-test-url`,
type: 'short-input',
readOnly: true,
showCopyButton: true,
}}
value={testUrl}
readOnly={true}
showCopyButton={true}
disabled={isPreview || disabled}
isPreview={isPreview}
/>
{testUrlExpiresAt && (
<p className='text-[12px] text-[var(--text-tertiary)]'>
Expires {new Date(testUrlExpiresAt).toLocaleString()}
</p>
)}
</>
) : (
<p className='text-muted-foreground text-xs'>
Generate a temporary URL that executes this webhook against the live (undeployed)
workflow state.
</p>
)}
{testUrlExpiresAt && (
<p className='text-muted-foreground text-xs'>
Expires at {new Date(testUrlExpiresAt).toLocaleString()}
<p className='text-[12px] text-[var(--text-tertiary)]'>
Generate a temporary URL to test against the live (undeployed) workflow state.
</p>
)}
</div>

View File

@@ -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<ScheduleInfo | null>(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,

View File

@@ -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<typeof workflowSchedule.$inferInsert>
type ExecutionCoreResult = Awaited<ReturnType<typeof executeWorkflowCore>>

View File

@@ -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<ScheduleData | null> {
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 })
},
})
}

View File

@@ -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'

View File

@@ -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

View File

@@ -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 {

View File

@@ -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