Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/content/docs/de/triggers/schedule.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/en/triggers/schedule.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/es/triggers/schedule.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/fr/triggers/schedule.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/ja/triggers/schedule.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import { Image } from '@/components/ui/image'

## 自動無効化

スケジュールは**10回連続で失敗**すると、エラーの連鎖を防ぐため自動的に無効化されます。無効化されると:
スケジュールは**100回連続で失敗**すると、エラーの連鎖を防ぐため自動的に無効化されます。無効化されると:

- スケジュールブロックに警告バッジが表示されます
- スケジュールの実行が停止します
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/zh/triggers/schedule.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import { Image } from '@/components/ui/image'

## 自动禁用

计划在连续 **10 次失败** 后会自动禁用,以防止错误持续发生。禁用后:
计划在连续 **100 次失败** 后会自动禁用,以防止错误持续发生。禁用后:

- 计划块上会显示警告徽章
- 计划将停止执行
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/schedules/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

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 (
<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>
)}

{/* Next run time */}
{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'}
{redeployMutation.isError && (
<p className='text-[12px] text-[var(--text-error)]'>
Failed to redeploy. Please try again.
</p>
)}
</div>
)}

{/* 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 info - only show when active */}
{!isDisabled && (
<div className='text-[12px] text-[var(--text-tertiary)]'>
{schedule?.cronExpression && (
<span>{parseCronToHumanReadable(schedule.cronExpression, timezone)}</span>
)}
{nextRunAt && (
<>
{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>
)}
Expand Down
Loading
Loading