(undefined);
+
+ useEffect(() => {
+ latestConditionsRef.current = conditions;
+ }, [conditions]);
+
+ useEffect(() => {
+ if (!focusRequest) return;
+ if (handledFocusRequestIDRef.current === focusRequest.requestID) return;
+
+ const focusedCondition = latestConditionsRef.current.find((condition) =>
+ conditionMatchesName(condition, focusRequest.conditionName),
+ );
+ if (!focusedCondition) return;
+
+ const row = conditionRowRefs.current.get(
+ getConditionFocusKey(focusedCondition),
+ );
+ row?.scrollIntoView?.({ behavior: "smooth", block: "center" });
+ row?.focus({ preventScroll: true });
+ handledFocusRequestIDRef.current = focusRequest.requestID;
+ }, [focusRequest]);
+
+ const registerConditionRow = (
+ condition: WaitTermView,
+ node: HTMLDivElement | null,
+ ) => {
+ const key = getConditionFocusKey(condition);
+ if (node) {
+ conditionRowRefs.current.set(key, node);
+ return;
+ }
+
+ conditionRowRefs.current.delete(key);
+ };
+
+ return (
+
+
+ {matchedConditions.length.toString()} of {conditions.length.toString()}{" "}
+ conditions satisfied
+
+
+
+
+ Status
+ Condition
+
+
+ {conditions.map((condition) => {
+ const conditionSignalState = condition.signal
+ ? signalListStates[getConditionSignalStateKey(condition)]
+ : undefined;
+ const conditionSignalsOpen =
+ condition.signal !== undefined &&
+ openSignalSurface?.kind === "condition" &&
+ getSignalSurfaceStateKey(openSignalSurface) ===
+ getConditionSignalStateKey(condition);
+
+ return (
+
+ );
+ })}
+
+
+
+ );
+};
+
+const ConditionRow = ({
+ condition,
+ focused,
+ onLoadMore,
+ onRegisterRow,
+ onToggleConditionSignals,
+ openSignalSurface,
+ signalListState,
+ wait,
+}: {
+ condition: WaitTermView;
+ focused: boolean;
+ onLoadMore: (surface: SignalHistorySurface) => void;
+ onRegisterRow: (condition: WaitTermView, node: HTMLDivElement | null) => void;
+ onToggleConditionSignals: (surface: SignalHistorySurface) => void;
+ openSignalSurface: SignalHistorySurface | undefined;
+ signalListState: SignalInspectorState;
+ wait: WorkflowTaskWait;
+}) => {
+ const stateTone = getConditionStateTone(condition, wait.phase);
+ const signal = condition.signal;
+ const timer = condition.timer;
+ const hasEvidence =
+ condition.dependencyTask !== undefined ||
+ signal !== undefined ||
+ timer !== undefined;
+ const showRawTechnicalName = Boolean(condition.exprCel || timer);
+ const metadataContent: ReactNode = timer ? (
+
+ ) : (
+ condition.technicalName
+ );
+
+ return (
+ onRegisterRow(condition, node)}
+ tabIndex={-1}
+ >
+
+
+
+
+ {getConditionStateLabel(condition, wait.phase)}
+
+
+
+
+
+ {condition.label}
+
+ {showRawTechnicalName ? (
+
+ {condition.technicalName}
+
+ ) : null}
+
+
+
+ •
+
+ {timer ? (
+ {metadataContent}
+ ) : condition.exprCel ? (
+
+ ) : (
+ {metadataContent}
+ )}
+
+
+
+ {hasEvidence ? (
+
+
+
+ ) : null}
+
+
+ {signal ? (
+
+ onToggleConditionSignals(signalSurfaceForCondition(condition))
+ }
+ open={
+ openSignalSurface?.kind === "condition" &&
+ getSignalSurfaceStateKey(openSignalSurface) ===
+ getConditionSignalStateKey(condition)
+ }
+ phase={wait.phase}
+ signal={signal}
+ signalListState={signalListState}
+ surface={signalSurfaceForCondition(condition)}
+ />
+ ) : null}
+
+ );
+};
+
+const ConditionExpression = ({
+ conditionLabel,
+ expression,
+}: {
+ conditionLabel: string;
+ expression: string;
+}) => {
+ const [expanded, setExpanded] = useState(false);
+ const expressionID = useId();
+ const isLongExpression =
+ expression.length > INLINE_CEL_MAX_LENGTH || /[\r\n]/.test(expression);
+
+ if (!isLongExpression) {
+ return (
+
+ {expression}
+
+ );
+ }
+
+ const previewExpression = getExpressionPreview(expression);
+ const buttonLabel = `${expanded ? "Hide" : "Show"} full CEL expression for ${conditionLabel}`;
+
+ return (
+ <>
+ setExpanded((current) => !current)}
+ title={expression}
+ type="button"
+ >
+ {previewExpression}
+ {expanded ? (
+
+ ) : (
+
+ )}
+
+ {expanded ? (
+
+ {expression}
+
+ ) : null}
+ >
+ );
+};
+
+const getExpressionPreview = (expression: string): string => {
+ const oneLineExpression = expression.replace(/\s+/g, " ").trim();
+ if (oneLineExpression.length <= INLINE_CEL_MAX_LENGTH) {
+ return `${oneLineExpression}...`;
+ }
+
+ return `${oneLineExpression.slice(0, INLINE_CEL_MAX_LENGTH)}...`;
+};
+
+const ConditionEvidence = ({
+ condition,
+ wait,
+}: {
+ condition: WaitTermView;
+ wait: WorkflowTaskWait;
+}) => {
+ const timer = condition.timer;
+ if (timer) {
+ return ;
+ }
+
+ const signal = condition.signal;
+ if (signal) {
+ return (
+
+ );
+ }
+
+ const dependencyTask = condition.dependencyTask;
+ if (dependencyTask) {
+ return (
+
+ );
+ }
+
+ return null;
+};
+
+const TimerConditionDefinition = ({
+ timer,
+}: {
+ timer: WorkflowTaskWait["inputs"]["timers"][number];
+}) => {
+ const delay = getTimerDelayLabel(timer);
+ const anchor = timer.anchor;
+
+ if (!delay) {
+ if (!anchor) return <>Immediate>;
+ switch (anchor.kind) {
+ case "task_finalized_at":
+ return anchor.task ? (
+ <>
+ When finalizes
+ >
+ ) : (
+ <>When dependency finalizes>
+ );
+ case "wait_started_at":
+ return <>When wait starts>;
+ case "workflow_created_at":
+ return <>When workflow starts>;
+ default:
+ return anchor.task ? (
+ <>
+ {anchor.kind.replaceAll("_", " ")} (
+ )
+ >
+ ) : (
+ <>{anchor.kind.replaceAll("_", " ")}>
+ );
+ }
+ }
+
+ if (!anchor) return <>After {delay}>;
+ switch (anchor.kind) {
+ case "task_finalized_at":
+ return anchor.task ? (
+ <>
+ {delay} after finalizes
+ >
+ ) : (
+ <>{delay} after dependency finalizes>
+ );
+ case "wait_started_at":
+ return <>{delay} after wait starts>;
+ case "workflow_created_at":
+ return <>{delay} after workflow starts>;
+ default:
+ return anchor.task ? (
+ <>
+ {delay} after {anchor.kind.replaceAll("_", " ")} (
+ )
+ >
+ ) : (
+ <>
+ {delay} after {anchor.kind.replaceAll("_", " ")}
+ >
+ );
+ }
+};
+
+const TimerTaskName = ({ taskName }: { taskName: string }) => (
+
+ {taskName}
+
+);
+
+const TimerConditionEvidence = ({
+ timer,
+}: {
+ timer: WorkflowTaskWait["inputs"]["timers"][number];
+}) => {
+ const fired = timer.result?.fired ?? false;
+ return (
+
+
+ {fired ? "Fired" : "Fires"}
+ {" "}
+
+
+ );
+};
+
+const DependencyConditionEvidence = ({
+ condition,
+ wait,
+}: {
+ condition: {
+ dependencyTask: WorkflowTask;
+ } & WaitTermView;
+ wait: WorkflowTaskWait;
+}) => {
+ if (condition.dependencyTask.finalizedAt) {
+ return (
+
+
+ Finalized
+ {" "}
+
+
+ );
+ }
+
+ return condition.matched ? (
+
+ ) : null;
+};
+
+const SignalConditionEvidence = ({
+ condition,
+ wait,
+}: {
+ condition: {
+ signal: WorkflowTaskWait["inputs"]["signals"][number];
+ } & WaitTermView;
+ wait: WorkflowTaskWait;
+}) => {
+ const signalResult = condition.signal.result;
+ const termResult = condition.result ?? undefined;
+ return (
+
+
+
+
+ {termResult ? (
+
+ ) : null}
+ {signalResult?.lastIncludedID ? (
+
+ ) : null}
+ {termResult?.lastMatchedID ? (
+
+ ) : null}
+
+ {condition.matched ? (
+
+ ) : null}
+
+ );
+};
+
+const ConditionSnapshotTiming = ({
+ label,
+ resolvedLabel,
+ wait,
+}: {
+ label: string;
+ resolvedLabel: string;
+ wait: WorkflowTaskWait;
+}) => {
+ const time = wait.resolvedAt ?? wait.evidence?.evaluatedAt;
+ if (!time) return null;
+
+ return (
+
+
+ {label}
+ {" "}
+ {wait.resolvedAt ? resolvedLabel : "by evaluation"}{" "}
+
+
+ );
+};
+
+const CompactEvidenceField = ({
+ label,
+ value,
+}: {
+ label: string;
+ value: ReactNode;
+}) => {
+ return (
+
+
{label}
+
+ {value}
+
+
+ );
+};
+
+const ConditionKindLabel = ({ kind }: { kind: string }) => {
+ return (
+
+
+ {getWaitTermKindLabel(kind)}
+
+ );
+};
+
+export const ConditionKindIcon = ({
+ className,
+ kind,
+}: {
+ className?: string;
+ kind: string;
+}) => {
+ switch (kind) {
+ case "dep_input":
+ case "generic":
+ return (
+
+ );
+ case "signal":
+ case "signal_input":
+ return (
+
+ );
+ case "timer":
+ case "timer_input":
+ return (
+
+ );
+ default:
+ return null;
+ }
+};
+
+const TimerTiming = ({
+ timer,
+}: {
+ timer: WorkflowTaskWait["inputs"]["timers"][number];
+}) => {
+ if (!timer.fireAt) {
+ return {formatTimerAnchorWait(timer.anchor)} ;
+ }
+
+ return ;
+};
diff --git a/src/components/WorkflowGateInspectorDiagnostics.tsx b/src/components/WorkflowGateInspectorDiagnostics.tsx
new file mode 100644
index 00000000..70aa4a56
--- /dev/null
+++ b/src/components/WorkflowGateInspectorDiagnostics.tsx
@@ -0,0 +1,125 @@
+import RelativeTimeFormatter from "@components/RelativeTimeFormatter";
+import { type WorkflowTaskWaitDiagnostics } from "@services/workflows";
+import { type ReactNode } from "react";
+
+import { type WaitDiagnosticsState } from "./WorkflowGateInspector.types";
+import { WaitSection } from "./WorkflowGateInspectorSummary";
+
+export const WaitDiagnosticsPanel = ({
+ diagnosticsState,
+}: {
+ diagnosticsState: WaitDiagnosticsState;
+}) => {
+ if (diagnosticsState.isLoading) {
+ return (
+
+
+ Loading current wait diagnostics…
+
+
+ );
+ }
+
+ if (diagnosticsState.error) {
+ return (
+
+
+ {diagnosticsState.error}
+
+
+ );
+ }
+
+ const diagnostics = diagnosticsState.value;
+ if (!diagnostics) return null;
+ const evalMessage = getDiagnosticsEvalMessage(diagnostics);
+
+ return (
+
+
+
Current status for declared wait inputs.
+
+
+
+ }
+ />
+
+
+
+ {diagnostics.truncated ? (
+
+ Signal diagnostics reached the scan limit, so expression and match
+ counts are best effort.
+
+ ) : null}
+ {evalMessage ? (
+
+ {evalMessage.message}
+
+ ) : null}
+
+
+ );
+};
+
+const CompactDiagnosticsField = ({
+ label,
+ value,
+}: {
+ label: string;
+ value: ReactNode;
+}) => (
+
+
+ {label}
+
+
+ {value}
+
+
+);
+
+const getDiagnosticsEvalMessage = (
+ diagnostics: WorkflowTaskWaitDiagnostics,
+): { message: string; tone: "neutral" | "warning" } | undefined => {
+ if (!diagnostics.evalError) return undefined;
+
+ const hasUnavailableDepOutput = diagnostics.inputs.deps.some(
+ (dep) => !dep.available,
+ );
+ if (diagnostics.phase === "not_started" && hasUnavailableDepOutput) {
+ return {
+ message: "Waiting for dependency output.",
+ tone: "neutral",
+ };
+ }
+
+ return {
+ message: diagnostics.evalError,
+ tone: "warning",
+ };
+};
diff --git a/src/components/WorkflowGateInspectorSignals.tsx b/src/components/WorkflowGateInspectorSignals.tsx
new file mode 100644
index 00000000..6234f59f
--- /dev/null
+++ b/src/components/WorkflowGateInspectorSignals.tsx
@@ -0,0 +1,262 @@
+import { Badge } from "@components/Badge";
+import JSONView from "@components/JSONView";
+import RelativeTimeFormatter from "@components/RelativeTimeFormatter";
+import { ChevronRightIcon } from "@heroicons/react/24/outline";
+import {
+ type WorkflowTaskSignal,
+ type WorkflowTaskWait,
+} from "@services/workflows";
+import clsx from "clsx";
+import { useState } from "react";
+
+import {
+ getLoadedSignalHistorySummary,
+ getSignalEvidenceSummary,
+} from "./WorkflowGateInspector.model";
+import {
+ type SignalHistorySurface,
+ type SignalInspectorState,
+} from "./WorkflowGateInspector.types";
+
+export const ConditionSignalEvidenceDisclosure = ({
+ onLoadMore,
+ onToggle,
+ open,
+ phase,
+ signal,
+ signalListState,
+ surface,
+}: {
+ onLoadMore: (surface: SignalHistorySurface) => void;
+ onToggle: () => void;
+ open: boolean;
+ phase: WorkflowTaskWait["phase"];
+ signal: WorkflowTaskWait["inputs"]["signals"][number];
+ signalListState: SignalInspectorState;
+ surface: SignalHistorySurface;
+}) => {
+ const scopeLabel =
+ phase === "resolved" ? "Resolution evidence" : "Signal history";
+ const signalSummary = getSignalEvidenceSummary(signal);
+
+ return (
+
+
+
+ {scopeLabel}
+
+ {signalSummary}
+
+
+
+ {open ? (
+ onLoadMore(surface)}
+ signalListState={signalListState}
+ />
+ ) : null}
+
+ );
+};
+
+export const AllTaskSignalsPanel = ({
+ onLoadMore,
+ onToggle,
+ open,
+ signalListState,
+}: {
+ onLoadMore: (surface: SignalHistorySurface) => void;
+ onToggle: () => void;
+ open: boolean;
+ signalListState: SignalInspectorState;
+}) => {
+ const signalSummary = getLoadedSignalHistorySummary(signalListState);
+
+ return (
+
+
+
+ All task signals
+ {signalSummary ? (
+
+ {signalSummary}
+
+ ) : null}
+
+
+ {open ? (
+
+ onLoadMore({ kind: "all" })}
+ signalListState={signalListState}
+ />
+
+ ) : null}
+
+ );
+};
+
+const SignalHistoryPanel = ({
+ emptyText,
+ helperText,
+ onLoadMore,
+ signalListState,
+}: {
+ emptyText: string;
+ helperText: string;
+ onLoadMore: () => void;
+ signalListState: SignalInspectorState;
+}) => {
+ const signalCount = signalListState.signals.length;
+
+ return (
+
+
{helperText}
+
+ {signalListState.error ? (
+
+ {signalListState.error}
+
+ ) : null}
+
+ {signalListState.isLoading ? (
+
+ Loading signal history…
+
+ ) : null}
+
+ {!signalListState.isLoading && signalListState.signals.length === 0 ? (
+
+ {emptyText}
+
+ ) : null}
+
+ {signalCount > 0 ? (
+
+ {signalListState.signals.map((signal) => (
+
+ ))}
+
+ ) : null}
+
+ {signalListState.hasMore ? (
+
+ {signalListState.isLoadingMore
+ ? "Loading older signals…"
+ : "Load older signals"}
+
+ ) : null}
+
+ );
+};
+
+const SignalHistoryItem = ({
+ defaultOpen,
+ signal,
+}: {
+ defaultOpen: boolean;
+ signal: WorkflowTaskSignal;
+}) => {
+ const [open, setOpen] = useState(defaultOpen);
+
+ return (
+ setOpen(event.currentTarget.open)}
+ open={open}
+ >
+
+
+ #{signal.id.toString()}
+
+ {signal.key}
+
+
+ workflow attempt {signal.attempt.toString()}
+
+
+
+
+
+
+
+
+ );
+};
+
+const SignalPayloadPanel = ({
+ copyTitle,
+ data,
+ title,
+}: {
+ copyTitle: string;
+ data: unknown;
+ title: string;
+}) => {
+ return (
+
+
+ {title}
+
+
+
+ );
+};
diff --git a/src/components/WorkflowGateInspectorSummary.tsx b/src/components/WorkflowGateInspectorSummary.tsx
new file mode 100644
index 00000000..c3705357
--- /dev/null
+++ b/src/components/WorkflowGateInspectorSummary.tsx
@@ -0,0 +1,151 @@
+import { Badge } from "@components/Badge";
+import RelativeTimeFormatter from "@components/RelativeTimeFormatter";
+import { type WorkflowTaskWait } from "@services/workflows";
+import { formatDurationShort } from "@utils/time";
+import { type ReactNode } from "react";
+
+import {
+ getWaitStatusLabel,
+ getWaitSummary,
+} from "./WorkflowGateInspector.model";
+import { type WaitTermView } from "./WorkflowGateInspector.types";
+
+export const WaitSummary = ({
+ matchedConditions,
+ onSelectCondition,
+ wait,
+}: {
+ matchedConditions: WaitTermView[];
+ onSelectCondition: (conditionName: string) => void;
+ wait: WorkflowTaskWait;
+}) => {
+ if (wait.phase === "resolved" && matchedConditions.length > 0) {
+ return (
+
+ Resolved by:{" "}
+
+ .
+
+ );
+ }
+
+ return (
+
+ {getWaitSummary(wait)}
+
+ );
+};
+
+const InlineConditionList = ({
+ conditions,
+ onSelectCondition,
+}: {
+ conditions: WaitTermView[];
+ onSelectCondition: (conditionName: string) => void;
+}) => {
+ return (
+ <>
+ {conditions.map((condition, index) => (
+
+ {index > 0 ? ", " : null}
+ onSelectCondition(condition.technicalName)}
+ type="button"
+ >
+ {condition.label}
+
+
+ ))}
+ >
+ );
+};
+
+export const WaitStatusPill = ({ wait }: { wait: WorkflowTaskWait }) => {
+ const color =
+ wait.phase === "resolved"
+ ? "green"
+ : wait.phase === "unknown"
+ ? "zinc"
+ : "amber";
+
+ return (
+
+ {getWaitStatusLabel(wait.phase)}
+
+ );
+};
+
+export const WaitSection = ({
+ children,
+ title,
+}: {
+ children: ReactNode;
+ title: string;
+}) => {
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+};
+
+export const WaitFacts = ({ wait }: { wait: WorkflowTaskWait }) => {
+ const items = [
+ wait.startedAt
+ ? {
+ label: "Started",
+ value: ,
+ }
+ : undefined,
+ wait.resolvedAt
+ ? {
+ label: "Resolved",
+ value: ,
+ }
+ : undefined,
+ wait.startedAt && wait.resolvedAt
+ ? {
+ label: "Waited",
+ value: formatDurationShort(wait.resolvedAt, wait.startedAt, false),
+ }
+ : undefined,
+ wait.evidence
+ ? {
+ label: "Evaluated",
+ value: (
+
+ ),
+ }
+ : undefined,
+ wait.evidence
+ ? {
+ label: "Workflow attempt",
+ value: wait.evidence.workflowAttempt.toString(),
+ }
+ : undefined,
+ ].filter((item) => item !== undefined);
+
+ if (items.length === 0) return null;
+
+ return (
+
+ {items.map((item) => (
+
+
+ {item.label}
+
+
+ {item.value}
+
+
+ ))}
+
+ );
+};
diff --git a/src/components/WorkflowList.stories.tsx b/src/components/WorkflowList.stories.tsx
new file mode 100644
index 00000000..16112e89
--- /dev/null
+++ b/src/components/WorkflowList.stories.tsx
@@ -0,0 +1,109 @@
+import type { WorkflowListItem } from "@services/workflows";
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import WorkflowList from "./WorkflowList";
+
+const meta: Meta = {
+ component: WorkflowList,
+ parameters: {
+ layout: "fullscreen",
+ router: {
+ routes: ["/", "/workflows", "/workflows/$workflowId"],
+ },
+ },
+ title: "Pages/WorkflowList",
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+const workflow = (
+ id: string,
+ name: string,
+ createdAt: string,
+ counts: Partial<
+ Pick<
+ WorkflowListItem,
+ | "countAvailable"
+ | "countCancelled"
+ | "countCompleted"
+ | "countDiscarded"
+ | "countFailedDeps"
+ | "countPending"
+ | "countRetryable"
+ | "countRunning"
+ | "countScheduled"
+ >
+ >,
+): WorkflowListItem => ({
+ countAvailable: counts.countAvailable ?? 0,
+ countCancelled: counts.countCancelled ?? 0,
+ countCompleted: counts.countCompleted ?? 0,
+ countDiscarded: counts.countDiscarded ?? 0,
+ countFailedDeps: counts.countFailedDeps ?? 0,
+ countPending: counts.countPending ?? 0,
+ countRetryable: counts.countRetryable ?? 0,
+ countRunning: counts.countRunning ?? 0,
+ countScheduled: counts.countScheduled ?? 0,
+ createdAt: new Date(createdAt),
+ id,
+ name,
+});
+
+const workflows: WorkflowListItem[] = [
+ workflow("wf-onboarding-2026-05-01", "Customer onboarding", "2026-05-01", {
+ countCompleted: 5,
+ countPending: 2,
+ countRunning: 1,
+ }),
+ workflow("wf-nightly-ledger-close", "Nightly ledger close", "2026-04-29", {
+ countCompleted: 12,
+ }),
+ workflow("wf-import-retry-queue", "Import retry queue", "2026-04-28", {
+ countCompleted: 8,
+ countFailedDeps: 1,
+ }),
+ workflow(
+ "wf-backfill-with-a-very-long-identifier-for-layout",
+ "Long-running historical backfill with a verbose display name",
+ "2026-04-22",
+ {
+ countCompleted: 21,
+ countPending: 4,
+ countScheduled: 3,
+ },
+ ),
+];
+
+export const Loading: Story = {
+ args: {
+ loading: true,
+ workflowItems: [],
+ workflowQueriesEnabled: true,
+ },
+};
+
+export const Populated: Story = {
+ args: {
+ loading: false,
+ workflowItems: workflows,
+ workflowQueriesEnabled: true,
+ },
+};
+
+export const NoWorkflows: Story = {
+ args: {
+ loading: false,
+ workflowItems: [],
+ workflowQueriesEnabled: true,
+ },
+};
+
+export const WorkflowsNotEnabled: Story = {
+ args: {
+ loading: false,
+ workflowItems: [],
+ workflowQueriesEnabled: false,
+ },
+};
diff --git a/src/components/WorkflowList.tsx b/src/components/WorkflowList.tsx
index fbf63d8f..67b76bb7 100644
--- a/src/components/WorkflowList.tsx
+++ b/src/components/WorkflowList.tsx
@@ -12,8 +12,8 @@ type StateTab = { name: string; state: undefined | WorkflowState };
type WorkflowListProps = {
loading: boolean;
- showingAll: boolean;
workflowItems: WorkflowListItem[];
+ workflowQueriesEnabled: boolean;
};
const tabs: StateTab[] = [
{ name: "All", state: undefined },
@@ -203,8 +203,8 @@ const WorkflowTable = ({
const WorkflowList = ({
loading,
- showingAll,
workflowItems,
+ workflowQueriesEnabled,
}: WorkflowListProps) => {
return (
@@ -225,7 +225,9 @@ const WorkflowList = ({
) : workflowItems.length > 0 ? (
) : (
-
+
)}