diff --git a/apps/sim/app/globals.css b/apps/sim/app/globals.css
index e108647e9f..5da83a32b2 100644
--- a/apps/sim/app/globals.css
+++ b/apps/sim/app/globals.css
@@ -74,30 +74,6 @@
animation: dash-animation 1.5s linear infinite !important;
}
-/**
- * Active block ring animation - cycles through gray tones using box-shadow
- */
-@keyframes ring-pulse-colors {
- 0%,
- 100% {
- box-shadow: 0 0 0 4px var(--surface-14);
- }
- 33% {
- box-shadow: 0 0 0 4px var(--surface-12);
- }
- 66% {
- box-shadow: 0 0 0 4px var(--surface-15);
- }
-}
-
-.dark .animate-ring-pulse {
- animation: ring-pulse-colors 2s ease-in-out infinite !important;
-}
-
-.light .animate-ring-pulse {
- animation: ring-pulse-colors 2s ease-in-out infinite !important;
-}
-
/**
* Dark color tokens - single source of truth for all colors (dark-only)
*/
@@ -135,6 +111,7 @@
--border-strong: #d1d1d1;
--divider: #e5e5e5;
--border-muted: #eeeeee;
+ --border-success: #d5d5d5;
/* Brand & state */
--brand-400: #8e4cfb;
@@ -250,6 +227,7 @@
--border-strong: #303030;
--divider: #393939;
--border-muted: #424242;
+ --border-success: #575757;
/* Brand & state */
--brand-400: #8e4cfb;
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
index c5a6f174d9..35d268c313 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
@@ -343,6 +343,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
handleClick,
hasRing,
ringStyles,
+ runPathStatus,
} = useBlockCore({ blockId: id, data, isPending })
const currentBlock = currentWorkflow.getBlockById(id)
@@ -750,21 +751,26 @@ export const WorkflowBlock = memo(function WorkflowBlock({
e.stopPropagation()
}}
>
-
+
-
+
{isWorkflowSelector && childWorkflowId && (
<>
{typeof childIsDeployed === 'boolean' ? (
@@ -890,6 +896,14 @@ export const WorkflowBlock = memo(function WorkflowBlock({
)}
+ {/* {isActive && (
+
+ )} */}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx
index 29581f4596..dcd94d4296 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx
@@ -1,6 +1,7 @@
import { X } from 'lucide-react'
import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getSmoothStepPath } from 'reactflow'
import type { EdgeDiffStatus } from '@/lib/workflows/diff/types'
+import { useExecutionStore } from '@/stores/execution/store'
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
interface WorkflowEdgeProps extends EdgeProps {
@@ -43,6 +44,7 @@ export const WorkflowEdge = ({
const diffAnalysis = useWorkflowDiffStore((state) => state.diffAnalysis)
const isShowingDiff = useWorkflowDiffStore((state) => state.isShowingDiff)
const isDiffReady = useWorkflowDiffStore((state) => state.isDiffReady)
+ const lastRunEdges = useExecutionStore((state) => state.lastRunEdges)
const generateEdgeIdentity = (
sourceId: string,
@@ -78,10 +80,16 @@ export const WorkflowEdge = ({
const dataSourceHandle = (data as { sourceHandle?: string } | undefined)?.sourceHandle
const isErrorEdge = (sourceHandle ?? dataSourceHandle) === 'error'
+ // Check if this edge was traversed during last execution
+ const edgeRunStatus = lastRunEdges.get(id)
+
const getEdgeColor = () => {
if (edgeDiffStatus === 'deleted') return 'var(--text-error)'
if (isErrorEdge) return 'var(--text-error)'
if (edgeDiffStatus === 'new') return 'var(--brand-tertiary)'
+ // Show run path status if edge was traversed
+ if (edgeRunStatus === 'success') return 'var(--border-success)'
+ if (edgeRunStatus === 'error') return 'var(--text-error)'
return 'var(--surface-12)'
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-core.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-core.ts
index 42f45e54f4..68dffa05ed 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-core.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-core.ts
@@ -1,10 +1,10 @@
import { useCallback, useMemo } from 'react'
-import { cn } from '@/lib/utils'
import { useExecutionStore } from '@/stores/execution/store'
import { usePanelEditorStore } from '@/stores/panel-new/editor/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useBlockState } from '../components/workflow-block/hooks'
import type { WorkflowBlockProps } from '../components/workflow-block/types'
+import { getBlockRingStyles } from '../utils/block-ring-utils'
import { useCurrentWorkflow } from './use-current-workflow'
interface UseBlockCoreOptions {
@@ -43,60 +43,19 @@ export function useBlockCore({ blockId, data, isPending = false }: UseBlockCoreO
}, [blockId, setCurrentBlockId])
// Ring styling based on all states
- // Priority: active (animated) > pending > focused > deleted > diff > run path
- const { hasRing, ringStyles } = useMemo(() => {
- const hasRing =
- isActive ||
- isPending ||
- isFocused ||
- diffStatus === 'new' ||
- diffStatus === 'edited' ||
- isDeletedBlock ||
- !!runPathStatus
-
- const ringStyles = cn(
- // Executing block: animated ring cycling through gray tones (animation handles all styling)
- isActive && 'animate-ring-pulse',
- // Non-active states use standard ring utilities
- !isActive && hasRing && 'ring-[1.75px]',
- // Pending state: warning ring
- !isActive && isPending && 'ring-[var(--warning)]',
- // Focused (selected) state: brand ring
- !isActive && !isPending && isFocused && 'ring-[var(--brand-secondary)]',
- // Deleted state (highest priority after active/pending/focused)
- !isActive && !isPending && !isFocused && isDeletedBlock && 'ring-[var(--text-error)]',
- // Diff states
- !isActive &&
- !isPending &&
- !isFocused &&
- !isDeletedBlock &&
- diffStatus === 'new' &&
- 'ring-[#22C55E]',
- !isActive &&
- !isPending &&
- !isFocused &&
- !isDeletedBlock &&
- diffStatus === 'edited' &&
- 'ring-[var(--warning)]',
- // Run path states (lowest priority - only show if no other states active)
- !isActive &&
- !isPending &&
- !isFocused &&
- !isDeletedBlock &&
- !diffStatus &&
- runPathStatus === 'success' &&
- 'ring-[var(--surface-14)]',
- !isActive &&
- !isPending &&
- !isFocused &&
- !isDeletedBlock &&
- !diffStatus &&
- runPathStatus === 'error' &&
- 'ring-[var(--text-error)]'
- )
-
- return { hasRing, ringStyles }
- }, [isActive, isPending, isFocused, diffStatus, isDeletedBlock, runPathStatus])
+ // Priority: active (executing) > pending > focused > deleted > diff > run path
+ const { hasRing, ringClassName: ringStyles } = useMemo(
+ () =>
+ getBlockRingStyles({
+ isActive,
+ isPending,
+ isFocused,
+ isDeletedBlock,
+ diffStatus,
+ runPathStatus,
+ }),
+ [isActive, isPending, isFocused, isDeletedBlock, diffStatus, runPathStatus]
+ )
return {
// Workflow context
@@ -116,5 +75,6 @@ export function useBlockCore({ blockId, data, isPending = false }: UseBlockCoreO
// Ring styling
hasRing,
ringStyles,
+ runPathStatus,
}
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts
index 7aaf72b1c2..1e34383832 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts
@@ -100,6 +100,7 @@ export function useWorkflowExecution() {
setDebugContext,
setActiveBlocks,
setBlockRunStatus,
+ setEdgeRunStatus,
} = useExecutionStore()
const [executionResult, setExecutionResult] = useState
(null)
const executionStream = useExecutionStream()
@@ -892,6 +893,12 @@ export function useWorkflowExecution() {
activeBlocksSet.add(data.blockId)
// Create a new Set to trigger React re-render
setActiveBlocks(new Set(activeBlocksSet))
+
+ // Track edges that led to this block as soon as execution starts
+ const incomingEdges = workflowEdges.filter((edge) => edge.target === data.blockId)
+ incomingEdges.forEach((edge) => {
+ setEdgeRunStatus(edge.id, 'success')
+ })
},
onBlockCompleted: (data) => {
@@ -904,6 +911,8 @@ export function useWorkflowExecution() {
// Track successful block execution in run path
setBlockRunStatus(data.blockId, 'success')
+ // Edges already tracked in onBlockStarted, no need to track again
+
// Add to console
addConsole({
input: data.input || {},
@@ -938,7 +947,6 @@ export function useWorkflowExecution() {
// Track failed block execution in run path
setBlockRunStatus(data.blockId, 'error')
-
// Add error to console
addConsole({
input: data.input || {},
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts
new file mode 100644
index 0000000000..b3b7ab814c
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts
@@ -0,0 +1,77 @@
+import { cn } from '@/lib/utils'
+
+export type BlockDiffStatus = 'new' | 'edited' | null | undefined
+
+export type BlockRunPathStatus = 'success' | 'error' | undefined
+
+export interface BlockRingOptions {
+ isActive: boolean
+ isPending: boolean
+ isFocused: boolean
+ isDeletedBlock: boolean
+ diffStatus: BlockDiffStatus
+ runPathStatus: BlockRunPathStatus
+}
+
+/**
+ * Derives visual ring visibility and class names for workflow blocks
+ * based on execution, focus, diff, deletion, and run-path states.
+ */
+export function getBlockRingStyles(options: BlockRingOptions): {
+ hasRing: boolean
+ ringClassName: string
+} {
+ const { isActive, isPending, isFocused, isDeletedBlock, diffStatus, runPathStatus } = options
+
+ const hasRing =
+ isActive ||
+ isPending ||
+ isFocused ||
+ diffStatus === 'new' ||
+ diffStatus === 'edited' ||
+ isDeletedBlock ||
+ !!runPathStatus
+
+ const ringClassName = cn(
+ // Executing block: pulsing success ring with prominent thickness
+ isActive && 'ring-[3.5px] ring-[var(--border-success)] animate-ring-pulse',
+ // Non-active states use standard ring utilities
+ !isActive && hasRing && 'ring-[1.75px]',
+ // Pending state: warning ring
+ !isActive && isPending && 'ring-[var(--warning)]',
+ // Focused (selected) state: brand ring
+ !isActive && !isPending && isFocused && 'ring-[var(--brand-secondary)]',
+ // Deleted state (highest priority after active/pending/focused)
+ !isActive && !isPending && !isFocused && isDeletedBlock && 'ring-[var(--text-error)]',
+ // Diff states
+ !isActive &&
+ !isPending &&
+ !isFocused &&
+ !isDeletedBlock &&
+ diffStatus === 'new' &&
+ 'ring-[#22C55E]',
+ !isActive &&
+ !isPending &&
+ !isFocused &&
+ !isDeletedBlock &&
+ diffStatus === 'edited' &&
+ 'ring-[var(--warning)]',
+ // Run path states (lowest priority - only show if no other states active)
+ !isActive &&
+ !isPending &&
+ !isFocused &&
+ !isDeletedBlock &&
+ !diffStatus &&
+ runPathStatus === 'success' &&
+ 'ring-[var(--border-success)]',
+ !isActive &&
+ !isPending &&
+ !isFocused &&
+ !isDeletedBlock &&
+ !diffStatus &&
+ runPathStatus === 'error' &&
+ 'ring-[var(--text-error)]'
+ )
+
+ return { hasRing, ringClassName }
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts
index 35be456e61..adf8b5f8a3 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts
@@ -1,2 +1,3 @@
export * from './auto-layout-utils'
+export * from './block-ring-utils'
export * from './workflow-execution-utils'
diff --git a/apps/sim/stores/execution/store.ts b/apps/sim/stores/execution/store.ts
index 8d1df0d94d..6c32355567 100644
--- a/apps/sim/stores/execution/store.ts
+++ b/apps/sim/stores/execution/store.ts
@@ -56,7 +56,7 @@ export const useExecutionStore = create()((se
if (isExecuting) {
set({ autoPanDisabled: false })
// Clear run path when starting a new execution
- set({ lastRunPath: new Map() })
+ set({ lastRunPath: new Map(), lastRunEdges: new Map() })
}
},
setIsDebugging: (isDebugging) => set({ isDebugging }),
@@ -69,6 +69,12 @@ export const useExecutionStore = create()((se
newRunPath.set(blockId, status)
set({ lastRunPath: newRunPath })
},
- clearRunPath: () => set({ lastRunPath: new Map() }),
+ setEdgeRunStatus: (edgeId, status) => {
+ const { lastRunEdges } = get()
+ const newRunEdges = new Map(lastRunEdges)
+ newRunEdges.set(edgeId, status)
+ set({ lastRunEdges: newRunEdges })
+ },
+ clearRunPath: () => set({ lastRunPath: new Map(), lastRunEdges: new Map() }),
reset: () => set(initialState),
}))
diff --git a/apps/sim/stores/execution/types.ts b/apps/sim/stores/execution/types.ts
index 0fc756f2cb..e374595b33 100644
--- a/apps/sim/stores/execution/types.ts
+++ b/apps/sim/stores/execution/types.ts
@@ -6,6 +6,11 @@ import type { ExecutionContext } from '@/executor/types'
*/
export type BlockRunStatus = 'success' | 'error'
+/**
+ * Represents the execution result of an edge in the last run
+ */
+export type EdgeRunStatus = 'success' | 'error'
+
export interface ExecutionState {
activeBlockIds: Set
isExecuting: boolean
@@ -16,9 +21,14 @@ export interface ExecutionState {
autoPanDisabled: boolean
/**
* Tracks blocks from the last execution run and their success/error status.
- * Cleared when a new run starts. Used to show run path indicators (green/red rings).
+ * Cleared when a new run starts. Used to show run path indicators (rings on blocks).
*/
lastRunPath: Map
+ /**
+ * Tracks edges from the last execution run and their success/error status.
+ * Cleared when a new run starts. Used to show run path indicators on edges.
+ */
+ lastRunEdges: Map
}
export interface ExecutionActions {
@@ -30,6 +40,7 @@ export interface ExecutionActions {
setDebugContext: (context: ExecutionContext | null) => void
setAutoPanDisabled: (disabled: boolean) => void
setBlockRunStatus: (blockId: string, status: BlockRunStatus) => void
+ setEdgeRunStatus: (edgeId: string, status: EdgeRunStatus) => void
clearRunPath: () => void
reset: () => void
}
@@ -43,6 +54,7 @@ export const initialState: ExecutionState = {
debugContext: null,
autoPanDisabled: false,
lastRunPath: new Map(),
+ lastRunEdges: new Map(),
}
// Types for panning functionality
diff --git a/apps/sim/tailwind.config.ts b/apps/sim/tailwind.config.ts
index 0219a1266f..eea19f4440 100644
--- a/apps/sim/tailwind.config.ts
+++ b/apps/sim/tailwind.config.ts
@@ -144,10 +144,10 @@ export default {
},
'ring-pulse': {
'0%, 100%': {
- opacity: '1',
+ 'box-shadow': '0 0 0 1.5px var(--border-success)',
},
'50%': {
- opacity: '0.6',
+ 'box-shadow': '0 0 0 4px var(--border-success)',
},
},
},