diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx
index bde89f51df8..4990fd6609e 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx
@@ -29,11 +29,13 @@ import {
ChevronUp,
Clipboard,
Search,
+ SquareArrowUpRight,
Workflow,
Wrench,
X,
} from '@sim/emcn/icons'
import { formatDuration } from '@sim/utils/formatting'
+import Link from 'next/link'
import { useParams, useRouter } from 'next/navigation'
import { useQueryState } from 'nuqs'
import { createPortal } from 'react-dom'
@@ -59,8 +61,10 @@ import {
DELETED_WORKFLOW_LABEL,
formatDate,
getDisplayStatus,
+ resolveLogWorkflowId,
StatusBadge,
TriggerBadge,
+ workflowEditorPath,
} from '@/app/workspace/[workspaceId]/logs/utils'
import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -317,6 +321,19 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP
const isWorkflowExecutionLog =
(log.trigger === 'manual' && !!log.duration) || !!log.executionData?.traceSpans
+ /**
+ * The workflow this run belongs to, when it is still reachable. Null for Sim
+ * agent jobs and deleted workflows, which render their label as static text.
+ */
+ const openableWorkflowId = resolveLogWorkflowId(log)
+
+ const workflowLabel =
+ log.trigger === 'mothership'
+ ? log.jobTitle || 'Untitled Job'
+ : openableWorkflowId
+ ? log.workflow?.name || 'Unknown'
+ : DELETED_WORKFLOW_LABEL
+
const hasCostInfo = !!(isWorkflowExecutionLog && log.cost)
const showWorkflowState =
isWorkflowExecutionLog &&
@@ -465,15 +482,31 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP
{log.trigger === 'mothership' ? 'Job' : 'Workflow'}
-
-
-
- {log.trigger === 'mothership'
- ? log.jobTitle || 'Untitled Job'
- : log.workflow?.name ||
- (!log.workflowId ? DELETED_WORKFLOW_LABEL : 'Unknown')}
-
-
+ {openableWorkflowId ? (
+
+
+
+
+
+
+ {workflowLabel}
+
+ (opens in a new tab)
+
+ ) : (
+
+
+
+ {workflowLabel}
+
+
+ )}
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx
index 0b2f7c11a2b..d0c8940bd8d 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-row-context-menu/log-row-context-menu.tsx
@@ -16,6 +16,7 @@ import {
X,
} from '@sim/emcn'
import type { WorkflowLogSummary } from '@/lib/api/contracts/logs'
+import { resolveLogWorkflowId } from '@/app/workspace/[workspaceId]/logs/utils'
interface LogRowContextMenuProps {
isOpen: boolean
@@ -58,6 +59,12 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
}: LogRowContextMenuProps) {
const hasExecutionId = Boolean(log?.executionId)
const hasWorkflow = Boolean(log?.workflow?.id || log?.workflowId)
+ /**
+ * "Open Workflow" needs a navigable target, which is stricter than
+ * `hasWorkflow`: Sim agent jobs have no workflow of their own. Cancel/retry
+ * keep using `hasWorkflow` so their gating is unchanged.
+ */
+ const hasOpenableWorkflow = Boolean(log && resolveLogWorkflowId(log))
const isCancellable =
(log?.status === 'running' || log?.status === 'pending') && hasExecutionId && hasWorkflow
const isRetryable = log?.status === 'failed' && hasWorkflow && log?.trigger !== 'mothership'
@@ -112,7 +119,7 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
-
+
Open Workflow
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
index 7bf6142e139..5d812699e7c 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
@@ -92,9 +92,11 @@ import {
getDisplayStatus,
type LogStatus,
parseDuration,
+ resolveLogWorkflowId,
STATUS_CONFIG,
StatusBadge,
TriggerBadge,
+ workflowEditorPath,
} from './utils'
const LOGS_PER_PAGE = 50 as const
@@ -524,9 +526,9 @@ export default function Logs() {
}, [contextMenuLog, workspaceId])
const handleOpenWorkflow = useCallback(() => {
- const wfId = contextMenuLog?.workflow?.id || contextMenuLog?.workflowId
+ const wfId = contextMenuLog ? resolveLogWorkflowId(contextMenuLog) : null
if (wfId) {
- window.open(`/workspace/${workspaceId}/w/${wfId}`, '_blank')
+ window.open(workflowEditorPath(workspaceId, wfId), '_blank')
}
}, [contextMenuLog, workspaceId])
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/utils.test.ts b/apps/sim/app/workspace/[workspaceId]/logs/utils.test.ts
new file mode 100644
index 00000000000..4d4cff37ec7
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/logs/utils.test.ts
@@ -0,0 +1,55 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { describe, expect, it } from 'vitest'
+import { resolveLogWorkflowId, workflowEditorPath } from './utils'
+
+describe('resolveLogWorkflowId', () => {
+ it('returns the nested workflow id when present', () => {
+ expect(
+ resolveLogWorkflowId({ trigger: 'manual', workflowId: 'wf-1', workflow: { id: 'wf-1' } })
+ ).toBe('wf-1')
+ })
+
+ it('falls back to workflowId when the workflow object is absent', () => {
+ expect(resolveLogWorkflowId({ trigger: 'api', workflowId: 'wf-2', workflow: null })).toBe(
+ 'wf-2'
+ )
+ })
+
+ it('prefers the nested workflow id over workflowId when both are set', () => {
+ expect(
+ resolveLogWorkflowId({ trigger: 'manual', workflowId: 'stale', workflow: { id: 'fresh' } })
+ ).toBe('fresh')
+ })
+
+ it('returns null for Sim agent jobs even when a workflow id exists', () => {
+ expect(
+ resolveLogWorkflowId({
+ trigger: 'mothership',
+ workflowId: 'wf-3',
+ workflow: { id: 'wf-3' },
+ })
+ ).toBeNull()
+ })
+
+ it('returns null for a deleted workflow (both id fields empty)', () => {
+ expect(resolveLogWorkflowId({ trigger: 'manual', workflowId: null, workflow: null })).toBeNull()
+ })
+
+ it('returns null when ids are present but empty strings', () => {
+ expect(
+ resolveLogWorkflowId({ trigger: 'manual', workflowId: '', workflow: { id: '' } })
+ ).toBeNull()
+ })
+
+ it('treats a missing trigger as a normal workflow run', () => {
+ expect(resolveLogWorkflowId({ workflowId: 'wf-4' })).toBe('wf-4')
+ })
+})
+
+describe('workflowEditorPath', () => {
+ it('builds the workspace-scoped editor path', () => {
+ expect(workflowEditorPath('ws-1', 'wf-1')).toBe('/workspace/ws-1/w/wf-1')
+ })
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts
index fbffba81702..05820985af3 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/utils.ts
+++ b/apps/sim/app/workspace/[workspaceId]/logs/utils.ts
@@ -17,6 +17,29 @@ export const LOG_COLUMNS = {
export const DELETED_WORKFLOW_LABEL = 'Deleted Workflow'
+/**
+ * Resolves the workflow a log row points at, or null when there is nowhere to
+ * navigate. Sim agent jobs have no workflow of their own, and a deleted
+ * workflow leaves both id fields empty.
+ *
+ * Single source of truth for "is this log's workflow reachable" — the list row,
+ * its context menu, and the details panel must agree, or a row can render as
+ * "Deleted Workflow" while still linking somewhere.
+ */
+export function resolveLogWorkflowId(log: {
+ trigger?: string | null
+ workflowId?: string | null
+ workflow?: { id?: string } | null
+}): string | null {
+ if (log.trigger === 'mothership') return null
+ return log.workflow?.id || log.workflowId || null
+}
+
+/** Path to a workflow in the editor. */
+export function workflowEditorPath(workspaceId: string, workflowId: string): string {
+ return `/workspace/${workspaceId}/w/${workflowId}`
+}
+
export type LogStatus =
| 'error'
| 'pending'