diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx
index 91c6c1dae38..fe4b889f540 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx
@@ -1,5 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
+import { useIsCurrentWorkflowOwner } from 'features/workflowLibrary/hooks/useIsCurrentWorkflowOwner';
import { useSaveOrSaveAsWorkflow } from 'features/workflowLibrary/hooks/useSaveOrSaveAsWorkflow';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -8,6 +9,7 @@ import { PiFloppyDiskBold } from 'react-icons/pi';
const SaveWorkflowButton = () => {
const { t } = useTranslation();
const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges();
+ const isCurrentWorkflowOwner = useIsCurrentWorkflowOwner();
const saveOrSaveAsWorkflow = useSaveOrSaveAsWorkflow();
return (
@@ -15,7 +17,7 @@ const SaveWorkflowButton = () => {
tooltip={t('workflows.saveWorkflow')}
aria-label={t('workflows.saveWorkflow')}
icon={}
- isDisabled={!doesWorkflowHaveUnsavedChanges}
+ isDisabled={!doesWorkflowHaveUnsavedChanges || !isCurrentWorkflowOwner}
onClick={saveOrSaveAsWorkflow}
pointerEvents="auto"
/>
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/SaveWorkflowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/SaveWorkflowButton.tsx
index 39a93e4a382..779d6f018ee 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/SaveWorkflowButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/SaveWorkflowButton.tsx
@@ -1,4 +1,6 @@
import { IconButton } from '@invoke-ai/ui-library';
+import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
+import { useIsCurrentWorkflowOwner } from 'features/workflowLibrary/hooks/useIsCurrentWorkflowOwner';
import { useSaveOrSaveAsWorkflow } from 'features/workflowLibrary/hooks/useSaveOrSaveAsWorkflow';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -7,12 +9,15 @@ import { PiFloppyDiskBold } from 'react-icons/pi';
const SaveWorkflowButton = () => {
const { t } = useTranslation();
const saveOrSaveAsWorkflow = useSaveOrSaveAsWorkflow();
+ const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges();
+ const isCurrentWorkflowOwner = useIsCurrentWorkflowOwner();
return (
}
+ isDisabled={!doesWorkflowHaveUnsavedChanges || !isCurrentWorkflowOwner}
onClick={saveOrSaveAsWorkflow}
pointerEvents="auto"
variant="ghost"
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx
index 6f5acc431ed..e683cfdbefd 100644
--- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx
+++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx
@@ -1,5 +1,6 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
+import { useIsCurrentWorkflowOwner } from 'features/workflowLibrary/hooks/useIsCurrentWorkflowOwner';
import { useSaveOrSaveAsWorkflow } from 'features/workflowLibrary/hooks/useSaveOrSaveAsWorkflow';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -9,11 +10,12 @@ const SaveWorkflowMenuItem = () => {
const { t } = useTranslation();
const saveOrSaveAsWorkflow = useSaveOrSaveAsWorkflow();
const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges();
+ const isCurrentWorkflowOwner = useIsCurrentWorkflowOwner();
return (
}
onClick={saveOrSaveAsWorkflow}
>
diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useIsCurrentWorkflowOwner.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useIsCurrentWorkflowOwner.ts
new file mode 100644
index 00000000000..5183c9050b7
--- /dev/null
+++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useIsCurrentWorkflowOwner.ts
@@ -0,0 +1,48 @@
+import { skipToken } from '@reduxjs/toolkit/query';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectCurrentUser } from 'features/auth/store/authSlice';
+import { selectWorkflowId } from 'features/nodes/store/selectors';
+import { useMemo } from 'react';
+import { useGetSetupStatusQuery } from 'services/api/endpoints/auth';
+import { useGetWorkflowQuery } from 'services/api/endpoints/workflows';
+
+/**
+ * Returns true if the current user can save the currently-loaded workflow directly (not as a copy).
+ *
+ * In single-user mode, this always returns true.
+ * In multiuser mode, returns true when:
+ * - The workflow has no ID (new, unsaved workflow — will open Save As)
+ * - The current user is the owner of the workflow
+ * - The current user is an admin
+ */
+export const useIsCurrentWorkflowOwner = (): boolean => {
+ const workflowId = useAppSelector(selectWorkflowId);
+ const currentUser = useAppSelector(selectCurrentUser);
+ const { data: setupStatus } = useGetSetupStatusQuery();
+ const { data: workflowData } = useGetWorkflowQuery(workflowId ?? skipToken);
+
+ return useMemo(() => {
+ // In single-user mode there is no concept of ownership, so saving is always allowed.
+ if (!setupStatus?.multiuser_enabled) {
+ return true;
+ }
+
+ // No authenticated user — be permissive.
+ if (!currentUser) {
+ return true;
+ }
+
+ // No workflow ID means this is a new/unsaved workflow. Clicking "Save" will open the
+ // Save As dialog, so we should not block it.
+ if (!workflowId) {
+ return true;
+ }
+
+ // API data not yet available — be permissive to avoid incorrect disabling during loading.
+ if (!workflowData) {
+ return true;
+ }
+
+ return workflowData.user_id === currentUser.user_id || currentUser.is_admin;
+ }, [setupStatus?.multiuser_enabled, workflowId, workflowData, currentUser]);
+};