Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
Input,
Tooltip,
} from '@/components/emcn'
import { Copy as CopyIcon, Search as SearchIcon } from '@/components/emcn/icons'
import { Copy as CopyIcon, Redo, Search as SearchIcon } from '@/components/emcn/icons'
import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants'
import { cn } from '@/lib/core/utils/cn'
import { formatDuration } from '@/lib/core/utils/formatting'
Expand Down Expand Up @@ -264,6 +264,10 @@ interface LogDetailsProps {
hasNext?: boolean
/** Whether there is a previous log available */
hasPrev?: boolean
/** Callback to retry a failed execution */
onRetryExecution?: () => void
/** Whether a retry is currently in progress */
isRetryPending?: boolean
}

/**
Expand All @@ -280,6 +284,8 @@ export const LogDetails = memo(function LogDetails({
onNavigatePrev,
hasNext = false,
hasPrev = false,
onRetryExecution,
isRetryPending = false,
}: LogDetailsProps) {
const [isExecutionSnapshotOpen, setIsExecutionSnapshotOpen] = useState(false)
const scrollAreaRef = useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -389,6 +395,22 @@ export const LogDetails = memo(function LogDetails({
>
<ChevronUp className='h-[14px] w-[14px] rotate-180' />
</Button>
{log?.status === 'failed' && (log?.workflow?.id || log?.workflowId) && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
className='!p-1'
onClick={() => onRetryExecution?.()}
disabled={isRetryPending}
aria-label='Retry execution'
>
<Redo className='h-[14px] w-[14px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>Retry</Tooltip.Content>
</Tooltip.Root>
)}
<Button variant='ghost' className='!p-1' onClick={onClose} aria-label='Close'>
<X className='h-[14px] w-[14px]' />
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/emcn'
import { Copy, Eye, Link, ListFilter, SquareArrowUpRight, X } from '@/components/emcn/icons'
import { Copy, Eye, Link, ListFilter, Redo, SquareArrowUpRight, X } from '@/components/emcn/icons'
import type { WorkflowLog } from '@/stores/logs/filters/types'

interface LogRowContextMenuProps {
Expand All @@ -23,6 +23,8 @@ interface LogRowContextMenuProps {
onToggleWorkflowFilter: () => void
onClearAllFilters: () => void
onCancelExecution: () => void
onRetryExecution: () => void
isRetryPending?: boolean
isFilteredByThisWorkflow: boolean
hasActiveFilters: boolean
}
Expand All @@ -43,13 +45,16 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
onToggleWorkflowFilter,
onClearAllFilters,
onCancelExecution,
onRetryExecution,
isRetryPending = false,
isFilteredByThisWorkflow,
hasActiveFilters,
}: LogRowContextMenuProps) {
const hasExecutionId = Boolean(log?.executionId)
const hasWorkflow = Boolean(log?.workflow?.id || log?.workflowId)
const isCancellable =
(log?.status === 'running' || log?.status === 'pending') && hasExecutionId && hasWorkflow
const isRetryable = log?.status === 'failed' && hasWorkflow

return (
<DropdownMenu open={isOpen} onOpenChange={(open) => !open && onClose()} modal={false}>
Expand All @@ -73,6 +78,15 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
sideOffset={4}
onCloseAutoFocus={(e) => e.preventDefault()}
>
{isRetryable && (
<>
<DropdownMenuItem onSelect={onRetryExecution} disabled={isRetryPending}>
<Redo />
{isRetryPending ? 'Retrying...' : 'Retry'}
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
{isCancellable && (
<>
<DropdownMenuItem onSelect={onCancelExecution}>
Expand Down
43 changes: 43 additions & 0 deletions apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
DropdownMenuTrigger,
Library,
Loader,
toast,
} from '@/components/emcn'
import { DatePicker } from '@/components/emcn/components/date-picker/date-picker'
import { dollarsToCredits } from '@/lib/billing/credits/conversion'
Expand Down Expand Up @@ -53,11 +54,14 @@ import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/provide
import { getBlock } from '@/blocks/registry'
import { useFolderMap, useFolders } from '@/hooks/queries/folders'
import {
fetchLogDetail,
logKeys,
prefetchLogDetail,
useCancelExecution,
useDashboardStats,
useLogDetail,
useLogsList,
useRetryExecution,
} from '@/hooks/queries/logs'
import { useWorkflowMap, useWorkflows } from '@/hooks/queries/workflows'
import { useDebounce } from '@/hooks/use-debounce'
Expand All @@ -74,6 +78,7 @@ import {
import {
DELETED_WORKFLOW_COLOR,
DELETED_WORKFLOW_LABEL,
extractRetryInput,
formatDate,
getDisplayStatus,
type LogStatus,
Expand Down Expand Up @@ -536,6 +541,7 @@ export default function Logs() {
}, [contextMenuLog])

const cancelExecution = useCancelExecution()
const retryExecution = useRetryExecution()

const handleCancelExecution = useCallback(() => {
const workflowId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId
Expand All @@ -546,6 +552,37 @@ export default function Logs() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [contextMenuLog])

const retryLog = useCallback(
async (log: WorkflowLog | null) => {
const workflowId = log?.workflow?.id || log?.workflowId
const logId = log?.id
if (!workflowId || !logId) return

try {
const detailLog = await queryClient.fetchQuery({
queryKey: logKeys.detail(logId),
queryFn: ({ signal }) => fetchLogDetail(logId, signal),
staleTime: 30 * 1000,
})
const input = extractRetryInput(detailLog)
await retryExecution.mutateAsync({ workflowId, input })
toast.success('Retry started')
} catch {
toast.error('Failed to retry execution')
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)

const handleRetryExecution = useCallback(() => {
retryLog(contextMenuLog)
}, [contextMenuLog, retryLog])

const handleRetrySidebarExecution = useCallback(() => {
retryLog(selectedLog)
}, [selectedLog, retryLog])
Comment thread
waleedlatif1 marked this conversation as resolved.

const contextMenuWorkflowId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId
const isFilteredByThisWorkflow = Boolean(
contextMenuWorkflowId && workflowIds.length === 1 && workflowIds[0] === contextMenuWorkflowId
Expand Down Expand Up @@ -783,6 +820,8 @@ export default function Logs() {
onNavigatePrev={handleNavigatePrev}
hasNext={selectedLogIndex < sortedLogs.length - 1}
hasPrev={selectedLogIndex > 0}
onRetryExecution={handleRetrySidebarExecution}
isRetryPending={retryExecution.isPending}
/>
),
[
Expand All @@ -791,6 +830,8 @@ export default function Logs() {
handleCloseSidebar,
handleNavigateNext,
handleNavigatePrev,
handleRetrySidebarExecution,
retryExecution.isPending,
selectedLogIndex,
sortedLogs.length,
]
Expand Down Expand Up @@ -1191,6 +1232,8 @@ export default function Logs() {
onOpenWorkflow={handleOpenWorkflow}
onOpenPreview={handleOpenPreview}
onCancelExecution={handleCancelExecution}
onRetryExecution={handleRetryExecution}
isRetryPending={retryExecution.isPending}
onToggleWorkflowFilter={handleToggleWorkflowFilter}
onClearAllFilters={handleClearAllFilters}
isFilteredByThisWorkflow={isFilteredByThisWorkflow}
Expand Down
35 changes: 35 additions & 0 deletions apps/sim/app/workspace/[workspaceId]/logs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Badge } from '@/components/emcn'
import { formatDuration } from '@/lib/core/utils/formatting'
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
import { getBlock } from '@/blocks/registry'
import type { WorkflowLog } from '@/stores/logs/filters/types'
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'

export const LOG_COLUMNS = {
Expand Down Expand Up @@ -422,3 +423,37 @@ export const formatDate = (dateString: string) => {
})(),
}
}

/**
* Extracts the original workflow input from a log entry for retry.
* Prefers the persisted `workflowInput` field (new logs), falls back to
* reconstructing from `executionState.blockStates` (old logs).
*/
export function extractRetryInput(log: WorkflowLog): unknown | undefined {
const execData = log.executionData as Record<string, unknown> | undefined
if (!execData) return undefined

if (execData.workflowInput !== undefined) {
return execData.workflowInput
}

const executionState = execData.executionState as
| {
blockStates?: Record<
string,
{ output?: unknown; executed?: boolean; executionTime?: number }
>
}
| undefined
if (!executionState?.blockStates) return undefined

// Starter/trigger blocks are pre-populated with executed: false and executionTime: 0,
// which distinguishes them from blocks that actually ran during execution.
for (const state of Object.values(executionState.blockStates)) {
if (state.executed === false && state.executionTime === 0 && state.output != null) {
return state.output
}
Comment thread
waleedlatif1 marked this conversation as resolved.
}

Comment thread
waleedlatif1 marked this conversation as resolved.
return undefined
}
33 changes: 32 additions & 1 deletion apps/sim/hooks/queries/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ async function fetchLogsPage(
}
}

async function fetchLogDetail(logId: string, signal?: AbortSignal): Promise<WorkflowLog> {
export async function fetchLogDetail(logId: string, signal?: AbortSignal): Promise<WorkflowLog> {
const response = await fetch(`/api/logs/${logId}`, { signal })

if (!response.ok) {
Expand Down Expand Up @@ -331,3 +331,34 @@ export function useCancelExecution() {
},
})
}

export function useRetryExecution() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ workflowId, input }: { workflowId: string; input?: unknown }) => {
const res = await fetch(`/api/workflows/${workflowId}/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input, triggerType: 'manual', stream: true }),
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw new Error(data.error || 'Failed to retry execution')
}
// The ReadableStream is lazy — start() only runs when read.
// Read one chunk to trigger execution, then cancel.
// Execution continues server-side after client disconnect.
const reader = res.body?.getReader()
if (reader) {
await reader.read()
reader.cancel()
}
return { started: true }
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: logKeys.lists() })
queryClient.invalidateQueries({ queryKey: logKeys.details() })
queryClient.invalidateQueries({ queryKey: [...logKeys.all, 'stats'] })
},
})
}
4 changes: 4 additions & 0 deletions apps/sim/lib/logs/execution/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export class ExecutionLogger implements IExecutionLoggerService {
models: NonNullable<WorkflowExecutionLog['executionData']['models']>
}
executionState?: SerializableExecutionState
workflowInput?: unknown
}): WorkflowExecutionLog['executionData'] {
const {
existingExecutionData,
Expand All @@ -94,6 +95,7 @@ export class ExecutionLogger implements IExecutionLoggerService {
completionFailure,
executionCost,
executionState,
workflowInput,
} = params
const traceSpanCount = countTraceSpans(traceSpans)

Expand Down Expand Up @@ -129,6 +131,7 @@ export class ExecutionLogger implements IExecutionLoggerService {
},
models: executionCost.models,
...(executionState ? { executionState } : {}),
...(workflowInput !== undefined ? { workflowInput } : {}),
}
}

Expand Down Expand Up @@ -377,6 +380,7 @@ export class ExecutionLogger implements IExecutionLoggerService {
completionFailure,
executionCost,
executionState,
workflowInput,
})

const [updatedLog] = await db
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/logs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export interface WorkflowExecutionLog {
>
executionState?: SerializableExecutionState
finalOutput?: any
workflowInput?: unknown
errorDetails?: {
blockId: string
blockName: string
Expand Down
Loading