diff --git a/desktop/src/features/workflows/ui/WorkflowCard.tsx b/desktop/src/features/workflows/ui/WorkflowCard.tsx
index d62def974..67c3d9979 100644
--- a/desktop/src/features/workflows/ui/WorkflowCard.tsx
+++ b/desktop/src/features/workflows/ui/WorkflowCard.tsx
@@ -11,6 +11,8 @@ import {
MoreHorizontal,
Pencil,
Play,
+ Power,
+ PowerOff,
Send,
SmilePlus,
Timer,
@@ -31,12 +33,14 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import {
getWorkflowCardLabel,
getWorkflowDescription,
getWorkflowDisplayStatus,
+ getWorkflowEnabled,
getWorkflowPrimaryAction,
getWorkflowPrimaryActionChannel,
getWorkflowPrimaryActionEmoji,
@@ -50,7 +54,9 @@ type WorkflowCardProps = {
workflow: Workflow;
channelName?: string;
isActive?: boolean;
+ isTogglingEnabled?: boolean;
onTrigger: (workflowId: string) => void;
+ onToggleEnabled: (workflow: Workflow) => void;
onEdit: (workflow: Workflow) => void;
onDuplicate: (workflow: Workflow) => void;
onDelete: (workflow: Workflow) => void;
@@ -182,12 +188,15 @@ export function WorkflowCard({
workflow,
channelName,
isActive = false,
+ isTogglingEnabled = false,
onTrigger,
+ onToggleEnabled,
onEdit,
onDuplicate,
onDelete,
}: WorkflowCardProps) {
const customEmoji = useCustomEmoji();
+ const isEnabled = getWorkflowEnabled(workflow.definition);
const displayStatus = getWorkflowDisplayStatus(workflow);
const description = getWorkflowDescription(workflow.definition);
const configuredTrigger = getWorkflowTriggerConfig(workflow.definition);
@@ -321,6 +330,18 @@ export function WorkflowCard({
Duplicate
+ onToggleEnabled(workflow)}
+ >
+ {isEnabled ? (
+
+ ) : (
+
+ )}
+ {isEnabled ? "Disable" : "Enable"}
+
+
onDelete(workflow)}
diff --git a/desktop/src/features/workflows/ui/WorkflowsView.tsx b/desktop/src/features/workflows/ui/WorkflowsView.tsx
index f05c91429..1ae3847d7 100644
--- a/desktop/src/features/workflows/ui/WorkflowsView.tsx
+++ b/desktop/src/features/workflows/ui/WorkflowsView.tsx
@@ -1,6 +1,7 @@
import { Plus, RefreshCw, Zap } from "lucide-react";
import * as React from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { stringify as yamlStringify } from "yaml";
import {
allWorkflowsQueryKey,
@@ -10,11 +11,16 @@ import { WorkflowCard } from "@/features/workflows/ui/WorkflowCard";
import { WorkflowDeleteDialog } from "@/features/workflows/ui/WorkflowDeleteDialog";
import { WorkflowDetailPanel } from "@/features/workflows/ui/WorkflowDetailPanel";
import { WorkflowDialog } from "@/features/workflows/ui/WorkflowDialog";
+import {
+ getWorkflowEnabled,
+ withWorkflowEnabled,
+} from "@/features/workflows/ui/workflowDefinition";
import type { Channel, Workflow } from "@/shared/api/types";
import {
deleteWorkflow,
getChannelsWorkflows,
triggerWorkflow,
+ updateWorkflow,
} from "@/shared/api/tauriWorkflows";
import { Button } from "@/shared/ui/button";
import { Skeleton } from "@/shared/ui/skeleton";
@@ -126,6 +132,26 @@ export function WorkflowsView({
},
});
+ const toggleEnabledMutation = useMutation({
+ mutationFn: (workflow: Workflow) =>
+ updateWorkflow(
+ workflow.id,
+ yamlStringify(
+ withWorkflowEnabled(
+ workflow.definition,
+ !getWorkflowEnabled(workflow.definition),
+ ),
+ ),
+ ),
+ onSuccess: () => {
+ void queryClient.invalidateQueries({
+ predicate: (query) =>
+ query.queryKey[0] === "workflows" ||
+ query.queryKey[0] === "workflows-all",
+ });
+ },
+ });
+
const triggerOne = triggerMutation.mutate;
const handleTrigger = React.useCallback(
(workflowId: string) => triggerOne(workflowId),
@@ -156,6 +182,12 @@ export function WorkflowsView({
[],
);
+ const toggleEnabled = toggleEnabledMutation.mutate;
+ const handleToggleEnabled = React.useCallback(
+ (workflow: Workflow) => toggleEnabled(workflow),
+ [toggleEnabled],
+ );
+
const handleDialogOpenChange = React.useCallback((open: boolean) => {
if (!open) {
setDialogState({ mode: "closed" });
@@ -235,10 +267,15 @@ export function WorkflowsView({
diff --git a/desktop/src/features/workflows/ui/workflowDefinition.test.mjs b/desktop/src/features/workflows/ui/workflowDefinition.test.mjs
index ffe3e01bd..ad3777e9d 100644
--- a/desktop/src/features/workflows/ui/workflowDefinition.test.mjs
+++ b/desktop/src/features/workflows/ui/workflowDefinition.test.mjs
@@ -4,6 +4,7 @@ import test from "node:test";
import {
getWorkflowCardLabel,
getWorkflowPrimaryActionEmoji,
+ withWorkflowEnabled,
} from "./workflowDefinition.ts";
test("builds a plain-language workflow card label", () => {
@@ -111,6 +112,23 @@ test("returns the primary add-reaction emoji for rich card rendering", () => {
);
});
+test("toggles workflow enabled state without mutating its definition", () => {
+ const definition = {
+ name: "deploy",
+ enabled: false,
+ trigger: { on: "message_posted" },
+ future_field: { keep: true },
+ };
+
+ assert.deepEqual(withWorkflowEnabled(definition, true), {
+ name: "deploy",
+ trigger: { on: "message_posted" },
+ future_field: { keep: true },
+ });
+ assert.deepEqual(withWorkflowEnabled(definition, false), definition);
+ assert.equal(definition.enabled, false);
+});
+
test("summarizes common and custom schedules", () => {
assert.equal(
getWorkflowCardLabel({
diff --git a/desktop/src/features/workflows/ui/workflowDefinition.ts b/desktop/src/features/workflows/ui/workflowDefinition.ts
index ee3799bff..db3dfe120 100644
--- a/desktop/src/features/workflows/ui/workflowDefinition.ts
+++ b/desktop/src/features/workflows/ui/workflowDefinition.ts
@@ -362,6 +362,19 @@ export function getWorkflowEnabled(
return definition.enabled !== false;
}
+export function withWorkflowEnabled(
+ definition: Record,
+ enabled: boolean,
+): Record {
+ const updated = { ...definition };
+ if (enabled) {
+ delete updated.enabled;
+ } else {
+ updated.enabled = false;
+ }
+ return updated;
+}
+
export function getWorkflowDisplayStatus(
workflow: Workflow,
): Workflow["status"] | "disabled" {
diff --git a/desktop/tests/e2e/workflows.spec.ts b/desktop/tests/e2e/workflows.spec.ts
index d83b53f04..1218c837f 100644
--- a/desktop/tests/e2e/workflows.spec.ts
+++ b/desktop/tests/e2e/workflows.spec.ts
@@ -922,6 +922,27 @@ test("captures disabled diff workflows in the list UI", async ({ page }) => {
await expect(card).toContainText("disabled");
});
+test("enables and disables a workflow from its card menu", async ({ page }) => {
+ const workflowName = `toggle_workflow_${Date.now()}`;
+
+ await navigateToWorkflows(page);
+ await createWorkflow(page, workflowName);
+
+ const card = page
+ .locator('[data-testid^="workflow-card-"]')
+ .filter({ hasText: workflowName })
+ .first();
+ const actions = card.getByRole("button", { name: "Workflow actions" });
+
+ await actions.click();
+ await page.getByRole("menuitem", { name: "Disable" }).click();
+ await expect(card.getByText("disabled", { exact: true })).toBeVisible();
+
+ await actions.click();
+ await page.getByRole("menuitem", { name: "Enable" }).click();
+ await expect(card.getByText("active", { exact: true })).toBeVisible();
+});
+
test("shows the webhook secret dialog after saving a webhook workflow", async ({
page,
}) => {