diff --git a/skyvern-frontend/src/routes/workflows/hooks/useWorkflowRunQuery.ts b/skyvern-frontend/src/routes/workflows/hooks/useWorkflowRunQuery.ts
index 5fad7d95..dba94835 100644
--- a/skyvern-frontend/src/routes/workflows/hooks/useWorkflowRunQuery.ts
+++ b/skyvern-frontend/src/routes/workflows/hooks/useWorkflowRunQuery.ts
@@ -40,6 +40,8 @@ function useWorkflowRunQuery() {
}
return false;
},
+ // required for OS-level notifications to work (workflow run completion)
+ refetchIntervalInBackground: true,
placeholderData: keepPreviousData,
refetchOnMount: (query) => {
if (!query.state.data) {
diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunStatusAlert.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunStatusAlert.tsx
new file mode 100644
index 00000000..f6166efd
--- /dev/null
+++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunStatusAlert.tsx
@@ -0,0 +1,267 @@
+import {
+ BellIcon,
+ ChevronDownIcon,
+ ChevronUpIcon,
+} from "@radix-ui/react-icons";
+import { useEffect, useRef, useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { HelpTooltip } from "@/components/HelpTooltip";
+import { type Status as WorkflowRunStatus } from "@/api/types";
+
+const deadStatuses = [
+ "canceled",
+ "completed",
+ "failed",
+ "terminated",
+ "timed_out",
+] as const;
+
+const liveStatuses = ["paused", "running"] as const;
+
+type EndingStatus = (typeof deadStatuses)[number];
+type LiveStatus = (typeof liveStatuses)[number];
+type WatchableStatus = EndingStatus | LiveStatus;
+
+interface Props {
+ status: WorkflowRunStatus;
+ title?: string;
+ visible: boolean;
+}
+
+function WorkflowRunStatusAlert({ status, title, visible }: Props) {
+ const [notifyIsOpen, setNotifyIsOpen] = useState(false);
+ const [statusesWatched, setStatusesWatched] = useState
>(
+ new Set(),
+ );
+ const [hasRequestedNotification, setHasRequestedNotification] = useState(
+ statusesWatched.size > 0,
+ );
+
+ const dropdownRef = useRef(null);
+
+ const hasAllEndingStatuses = deadStatuses.every((s) =>
+ statusesWatched.has(s),
+ );
+
+ // Handle click outside to close dropdown
+ useEffect(() => {
+ if (!notifyIsOpen) {
+ return;
+ }
+
+ function handleClickOutside(event: MouseEvent) {
+ if (
+ dropdownRef.current &&
+ !dropdownRef.current.contains(event.target as Node)
+ ) {
+ setNotifyIsOpen(false);
+ }
+ }
+
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [notifyIsOpen]);
+
+ // Handle ESC key to close dropdown
+ useEffect(() => {
+ if (!notifyIsOpen) {
+ return;
+ }
+
+ function handleEscKey(event: KeyboardEvent) {
+ if (event.key === "Escape") {
+ setNotifyIsOpen(false);
+ }
+ }
+
+ document.addEventListener("keydown", handleEscKey);
+ return () => {
+ document.removeEventListener("keydown", handleEscKey);
+ };
+ }, [notifyIsOpen]);
+
+ useEffect(() => {
+ if (!hasRequestedNotification) {
+ return;
+ }
+
+ if (!statusesWatched.has(status as WatchableStatus)) {
+ return;
+ }
+
+ const audio = new Audio("/dragon-cry.mp3");
+
+ audio.play().catch((error) => {
+ console.error("Failed to play notification sound:", error);
+ });
+
+ if (Notification.permission === "granted") {
+ try {
+ const notification = new Notification(
+ `Workflow Run Status Change: ${status}`,
+ {
+ body: `The workflow run "${title ?? "unknown"}" has changed to status: ${status}`,
+ icon: "/favicon.png",
+ tag: `workflow-${title ?? "unknown"}-${status}`,
+ requireInteraction: false,
+ },
+ );
+
+ notification.onclick = () => {
+ window.focus();
+ notification.close();
+ };
+ } catch (error) {
+ console.error("Failed to create notification:", error);
+ }
+ } else {
+ console.warn(
+ "Notification permission not granted:",
+ Notification.permission,
+ );
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [status]);
+
+ function askForPermissions() {
+ if (hasRequestedNotification) {
+ return;
+ }
+
+ if (Notification.permission === "default") {
+ Notification.requestPermission().then((permission) => {
+ if (permission === "granted") {
+ setHasRequestedNotification(true);
+ }
+ });
+ } else if (Notification.permission === "granted") {
+ setHasRequestedNotification(true);
+ }
+ }
+
+ const notifySuffix =
+ statusesWatched.size === 0
+ ? null
+ : statusesWatched.size === 1
+ ? `(${(Array.from(statusesWatched)[0] ?? "").replace("_", " ")})`
+ : statusesWatched.size === deadStatuses.length &&
+ deadStatuses.every((s) => statusesWatched.has(s))
+ ? "(ending)"
+ : statusesWatched.size === liveStatuses.length + deadStatuses.length
+ ? "(any)"
+ : `(${statusesWatched.size} statuses)`;
+
+ if (!visible) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+export { WorkflowRunStatusAlert };