Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
158 commits
Select commit Hold shift + click to select a range
66ba32b
feat: Per-user workflow libraries in multiuser mode (#114)
Copilot Mar 6, 2026
c9f2e2d
Restrict model sync to admin users only (#118)
Copilot Mar 6, 2026
1ed9349
feat: distinct splash screens for admin/non-admin users in multiuser …
Copilot Mar 6, 2026
a4bbccd
Merge branch 'lstein/feature/multiuser-splash-screens' into lstein/fe…
lstein Mar 6, 2026
d965d60
Merge branch 'main' into lstein/feature/workflow-isolation-in-multius…
lstein Mar 6, 2026
b37bc8d
Disable Save when editing another user's shared workflow in multiuser…
Copilot Mar 6, 2026
298ce54
Merge branch 'main' into lstein/feature/workflow-isolation-in-multius…
lstein Mar 9, 2026
3dbc290
chore(app): ruff
lstein Mar 9, 2026
13faa0f
Add board visibility (private/shared/public) feature with tests and UI
Copilot Mar 9, 2026
f38d1ab
Enforce read-only access for non-owners of shared/public boards in UI
Copilot Mar 9, 2026
9f8f7a1
Fix remaining board access enforcement: invoke icon, drag-out, change…
Copilot Mar 10, 2026
74c293e
chore: merge and resolve conflicts
lstein Apr 4, 2026
3559a10
chore: merge with upstream
lstein Apr 4, 2026
f128121
fix: allow drag from shared boards to non-board targets (viewer, ref …
lstein Apr 4, 2026
b4276fd
Merge branch 'lstein/feature/workflow-isolation-in-multiuser-mode' in…
lstein Apr 4, 2026
23ab8f5
Merge branch 'main' into copilot/enhancement-allow-shared-boards
JPPhoto Apr 5, 2026
ac4ef09
fix(security): add auth requirement to all sensitive routes in multim…
lstein Apr 6, 2026
24d0d38
Merge branch 'main' into copilot/enhancement-allow-shared-boards
lstein Apr 6, 2026
5ba03e9
Merge branch 'main' into copilot/enhancement-allow-shared-boards
JPPhoto Apr 7, 2026
915239b
Merge branch 'main' into copilot/enhancement-allow-shared-boards
JPPhoto Apr 7, 2026
61c884c
Merge remote-tracking branch 'refs/remotes/origin/copilot/enhancement…
lstein Apr 7, 2026
ac1f1a5
chore(backend): ruff
lstein Apr 7, 2026
ed45bd4
Merge branch 'main' into copilot/enhancement-allow-shared-boards
JPPhoto Apr 8, 2026
c2e7a5d
Add workflow live update events
JPPhoto Apr 8, 2026
55b050e
Add persisted call-saved-workflows node stub
JPPhoto Apr 8, 2026
a4e3ac5
Add saved workflow picker state handling
JPPhoto Apr 8, 2026
5525ac0
Clarify saved workflow missing state
JPPhoto Apr 8, 2026
61c82c9
Validate selected saved workflows at runtime
JPPhoto Apr 8, 2026
e7496e6
Generalize saved workflow picker into a field type
JPPhoto Apr 8, 2026
ac0ca60
Harden saved workflow field behavior
JPPhoto Apr 8, 2026
b2a2007
Rename saved workflow node to singular
JPPhoto Apr 8, 2026
b3657d1
Fix saved workflow frontend build issues
JPPhoto Apr 8, 2026
37ca6b9
Add dynamic saved workflow form fields
JPPhoto Apr 9, 2026
6218863
Seed and handle dynamic workflow fields
JPPhoto Apr 9, 2026
ae8777f
Align dynamic workflow fields with node styling
JPPhoto Apr 9, 2026
c48732e
Use standard templates for dynamic workflow fields
JPPhoto Apr 9, 2026
381da4d
Revert stale dynamic edge graph guard
JPPhoto Apr 9, 2026
71389b6
Preserve inbound edges when compatbile with a new selected workflow
JPPhoto Apr 9, 2026
c85e009
Add call saved workflow design document
JPPhoto Apr 9, 2026
981b2b8
Clarify workflow return runtime design
JPPhoto Apr 9, 2026
3d991ea
Add workflow return node contract
JPPhoto Apr 9, 2026
4e6f848
Allow bounded recursive workflow calls
JPPhoto Apr 9, 2026
edd1258
Merge branch 'main' into copilot/enhancement-allow-shared-boards
JPPhoto Apr 9, 2026
55649c8
Merge branch 'pr-9018-updated' into call-saved-workflows
JPPhoto Apr 9, 2026
b86e289
fix (backend): improve user isolation for session queue and recall pa…
lstein Apr 10, 2026
797638b
fix(workflow): do not filter default workflows in multiuser mode
lstein Apr 10, 2026
8f792fc
Merge branch 'main' into copilot/enhancement-allow-shared-boards
lstein Apr 10, 2026
c7eeb26
Merge branch 'main' into copilot/enhancement-allow-shared-boards
JPPhoto Apr 10, 2026
3b813f1
Merge branch 'pr-9018-updated' into call-saved-workflows
JPPhoto Apr 10, 2026
d646bd1
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 10, 2026
21dd70b
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 10, 2026
7d863c3
Merge remote-tracking branch 'origin/main' into call-saved-workflows
JPPhoto Apr 12, 2026
c0c8541
Merge remote-tracking branch 'origin/main' into call-saved-workflows
JPPhoto Apr 13, 2026
d657e95
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 14, 2026
726ea31
Merge remote-tracking branch 'origin/main' into call-saved-workflows
JPPhoto Apr 14, 2026
fb1a2d6
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 14, 2026
ebe8f33
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 14, 2026
04cf7be
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 14, 2026
44b2a15
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 17, 2026
da871f7
Merge branch 'invoke-ai:main' into call-saved-workflows
JPPhoto Apr 17, 2026
c3a79a4
Fix frontend lint on workflow node fixtures
JPPhoto Apr 17, 2026
eacc185
Add workflow call runtime state scaffolding
JPPhoto Apr 17, 2026
e71c140
Add workflow call runner boundary handling
JPPhoto Apr 17, 2026
75c3f92
Create child workflow execution state on call boundary
JPPhoto Apr 17, 2026
1caf6ff
Update workflow call runtime documentation
JPPhoto Apr 17, 2026
4e5c045
Localize saved workflow node UI strings
JPPhoto Apr 17, 2026
39f6c17
Execute saved workflow children inline
JPPhoto Apr 17, 2026
20f63d5
chore: ruff
JPPhoto Apr 17, 2026
9fb18d6
Test inline saved workflow execution semantics
JPPhoto Apr 17, 2026
5152aa0
Fix saved workflow lint and schema warnings
JPPhoto Apr 17, 2026
b3ec622
Updated documentation
JPPhoto Apr 17, 2026
3accbb2
Propagate workflow return values to callers
JPPhoto Apr 18, 2026
2eada3f
Merge branch 'invoke-ai:main' into call-saved-workflows
JPPhoto Apr 18, 2026
5c159ac
Merge branch 'invoke-ai:main' into call-saved-workflows
JPPhoto Apr 18, 2026
6cb6afa
Merge branch 'invoke-ai:main' into call-saved-workflows
JPPhoto Apr 19, 2026
9ff1c75
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 19, 2026
4bfeb3b
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 19, 2026
7ea95e4
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 20, 2026
2fff1ea
Merge remote-tracking branch 'origin/main' into call-saved-workflows
JPPhoto Apr 20, 2026
531466e
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 20, 2026
8a5987e
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 20, 2026
460a3cd
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 20, 2026
a957869
Fix lazy If branch pruning and skipped-parent handling in graph runtime
JPPhoto Apr 20, 2026
1102655
Extract workflow call coordinator
JPPhoto Apr 21, 2026
2ca78ad
Track workflow call lifecycle state
JPPhoto Apr 21, 2026
80cb688
Merge branch 'fix-if-branch-edge-pruning' into call-saved-workflows
JPPhoto Apr 21, 2026
d3c3f5b
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 21, 2026
4e6a8fb
Add workflow call queue item metadata
JPPhoto Apr 21, 2026
3020328
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 21, 2026
7f50629
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 21, 2026
d769a92
Add workflow call queue item migration
JPPhoto Apr 21, 2026
c5d7b02
Queue child workflow executions and suspend parents
JPPhoto Apr 21, 2026
45fd036
Cascade workflow call cancel and root retry
JPPhoto Apr 21, 2026
e73961d
Refine workflow call queue UI and docs
JPPhoto Apr 21, 2026
ea7d8fb
Harden workflow call queue API behavior
JPPhoto Apr 21, 2026
03138ed
Harden workflow call retry event delivery
JPPhoto Apr 21, 2026
862333b
Extract workflow call runtime module
JPPhoto Apr 21, 2026
17697c5
Format workflow call queue migration
JPPhoto Apr 22, 2026
13b1f78
Validate callable workflow compatibility
JPPhoto Apr 22, 2026
294b9d4
Support direct batched child workflows
JPPhoto Apr 22, 2026
496d069
Merge branch 'invoke-ai:main' into call-saved-workflows
JPPhoto Apr 22, 2026
880d777
Support generator-backed batched child workflows
JPPhoto Apr 22, 2026
669a345
Split workflow call runtime tests
JPPhoto Apr 22, 2026
3fa755e
Clarify workflow call batch semantics
JPPhoto Apr 22, 2026
71713e1
Expose callable workflow compatibility metadata
JPPhoto Apr 22, 2026
9f84892
Surface callable workflow compatibility in library UI
JPPhoto Apr 22, 2026
406d7b4
Fix workflow call privacy and compatibility edge cases
JPPhoto Apr 22, 2026
677b0e4
Harden workflow call queue boundary and cleanup semantics
JPPhoto Apr 23, 2026
25dd7f2
Harden workflow call cancel retry and batch edge cases
JPPhoto Apr 23, 2026
cb0e737
Merge remote-tracking branch 'origin/main' into `call-saved-workflows`
JPPhoto Apr 23, 2026
59d58ad
Repaired database migration
JPPhoto Apr 23, 2026
18639d6
bring documentation back in line with the tested current state
JPPhoto Apr 23, 2026
150d41f
Harden saved workflow frontend compatibility state
JPPhoto Apr 23, 2026
896e50c
Treat waiting queue items as nonterminal in staging area
JPPhoto Apr 23, 2026
2d9e4dd
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 25, 2026
a508da8
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 25, 2026
cbbcff5
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 27, 2026
d0824c8
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 27, 2026
445b1f9
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 27, 2026
5132028
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 27, 2026
c1f63a1
Merge remote-tracking branch 'origin/main' into call-saved-workflows
JPPhoto Apr 27, 2026
84a329f
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 27, 2026
d72d2ea
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 27, 2026
3310554
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 27, 2026
9e92330
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 27, 2026
74ab0ea
chore: typegen
JPPhoto Apr 27, 2026
afbe634
Harden workflow call child propagation
JPPhoto Apr 28, 2026
77e0184
Tighten workflow call compatibility reporting
JPPhoto Apr 28, 2026
40833d5
Removed to-do items that have been done
JPPhoto Apr 28, 2026
f64205f
Update docs
JPPhoto Apr 28, 2026
1fbd119
Documented `WorkflowCallQueueLifecycle` as an intentional bounded com…
JPPhoto Apr 28, 2026
31755ac
Fix saved workflow selection fallback
JPPhoto Apr 29, 2026
78555fd
Fix workflow call startup cancellation
JPPhoto Apr 29, 2026
934c1eb
Fix single-user workflow socket rooms
JPPhoto Apr 29, 2026
8c230d1
Ignore stale saved workflow input payloads
JPPhoto Apr 29, 2026
5aeb3f3
Fix saved workflow picker discovery
JPPhoto Apr 29, 2026
5101326
Preserve workflow call batch field values
JPPhoto Apr 29, 2026
86ddbf2
chore: linting
JPPhoto Apr 29, 2026
8de8732
Harden workflow-call queue cancellation semantics
JPPhoto Apr 29, 2026
51fc375
Harden saved workflow caller UI state
JPPhoto Apr 29, 2026
a3c4507
Avoid eager workflow compatibility expansion
JPPhoto Apr 29, 2026
3a0a7f1
Fix workflow call README list nesting
JPPhoto Apr 29, 2026
eed386f
chore: mdformat
JPPhoto Apr 29, 2026
628e123
Updated documentation
JPPhoto Apr 29, 2026
e304ad5
Removed the anonymous collection from the workflow return production …
JPPhoto Apr 29, 2026
fefebb2
chore: lint
JPPhoto Apr 29, 2026
b9707df
Remove noise in tests
JPPhoto Apr 29, 2026
a40ae52
Bugfixes and optimization - workflow_return can now take a single or …
JPPhoto Apr 29, 2026
163f459
Bugfixes
JPPhoto Apr 29, 2026
a13307f
Fixed a persistence failure
JPPhoto Apr 29, 2026
821f3b9
Fix saved workflow call field rehydration
JPPhoto Apr 29, 2026
8b18ce0
Preserve workflow call completion state on resume
JPPhoto Apr 29, 2026
11d3eb7
Update queue status from socket events
JPPhoto Apr 30, 2026
b12746e
Updated tests to match the current workflow-return contract
JPPhoto Apr 30, 2026
9ec8278
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto Apr 30, 2026
e2e2fbd
Fix workflow child queue status isolation
JPPhoto Apr 30, 2026
81f2a63
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto May 1, 2026
9af5740
Merge remote-tracking branch 'origin/main' into codex-tmp/update-bran…
JPPhoto May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
795 changes: 795 additions & 0 deletions docs/contributing/call_saved_workflow.md

Large diffs are not rendered by default.

42 changes: 29 additions & 13 deletions invokeai/app/api/routers/session_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ def sanitize_queue_item_for_user(
sanitized_item.priority = 0
sanitized_item.field_values = None
sanitized_item.retried_from_item_id = None
sanitized_item.workflow_call_id = None
sanitized_item.parent_item_id = None
sanitized_item.parent_session_id = None
sanitized_item.root_item_id = None
sanitized_item.workflow_call_depth = None
sanitized_item.workflow = None
sanitized_item.error_type = None
sanitized_item.error_message = None
Expand Down Expand Up @@ -312,20 +317,27 @@ async def retry_items_by_id(
) -> RetryItemsResult:
"""Retries the given queue items. Users can only retry their own items unless they are an admin."""
try:
# Check authorization: user must own all items or be an admin
if not current_user.is_admin:
for item_id in item_ids:
try:
queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
if queue_item.user_id != current_user.user_id:
raise HTTPException(
status_code=403, detail=f"You do not have permission to retry queue item {item_id}"
)
except SessionQueueItemNotFoundError:
# Skip items that don't exist - they will be handled by retry_items_by_id
continue
# Check queue membership for all items and ownership for non-admins.
valid_item_ids: list[int] = []
for item_id in item_ids:
try:
queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
if queue_item.queue_id != queue_id:
raise HTTPException(
status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}"
)
if not current_user.is_admin and queue_item.user_id != current_user.user_id:
raise HTTPException(
status_code=403, detail=f"You do not have permission to retry queue item {item_id}"
)
valid_item_ids.append(item_id)
except SessionQueueItemNotFoundError:
# Skip items that don't exist - they will be handled by retry_items_by_id
continue

return ApiDependencies.invoker.services.session_queue.retry_items_by_id(queue_id=queue_id, item_ids=item_ids)
return ApiDependencies.invoker.services.session_queue.retry_items_by_id(
queue_id=queue_id, item_ids=valid_item_ids
)
except HTTPException:
raise
except Exception as e:
Expand Down Expand Up @@ -507,6 +519,8 @@ async def delete_queue_item(
try:
# Get the queue item to check ownership
queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
if queue_item.queue_id != queue_id:
raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")

# Check authorization: user must own the item or be an admin
if queue_item.user_id != current_user.user_id and not current_user.is_admin:
Expand Down Expand Up @@ -537,6 +551,8 @@ async def cancel_queue_item(
try:
# Get the queue item to check ownership
queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
if queue_item.queue_id != queue_id:
raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")

# Check authorization: user must own the item or be an admin
if queue_item.user_id != current_user.user_id and not current_user.is_admin:
Expand Down
81 changes: 67 additions & 14 deletions invokeai/app/api/routers/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.shared.pagination import PaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.shared.workflow_call_compatibility import get_workflow_call_compatibility
from invokeai.app.services.workflow_records.workflow_records_common import (
Workflow,
WorkflowCategory,
Expand Down Expand Up @@ -51,7 +52,18 @@ async def get_workflow(
raise HTTPException(status_code=403, detail="Not authorized to access this workflow")

thumbnail_url = ApiDependencies.invoker.services.workflow_thumbnails.get_url(workflow_id)
return WorkflowRecordWithThumbnailDTO(thumbnail_url=thumbnail_url, **workflow.model_dump())
compatibility = get_workflow_call_compatibility(
workflow=workflow.workflow.model_dump(),
workflow_id=workflow.workflow_id,
services=ApiDependencies.invoker.services,
user_id=current_user.user_id,
maximum_children=ApiDependencies.invoker.services.configuration.max_queue_size,
)
return WorkflowRecordWithThumbnailDTO(
thumbnail_url=thumbnail_url,
call_saved_workflow_compatibility=compatibility,
**workflow.model_dump(),
)


@workflows_router.patch(
Expand All @@ -66,17 +78,24 @@ async def update_workflow(
workflow: Workflow = Body(description="The updated workflow", embed=True),
) -> WorkflowRecordDTO:
"""Updates a workflow"""
try:
existing = ApiDependencies.invoker.services.workflow_records.get(workflow.id)
except WorkflowNotFoundError:
raise HTTPException(status_code=404, detail="Workflow not found")

config = ApiDependencies.invoker.services.configuration
if config.multiuser:
try:
existing = ApiDependencies.invoker.services.workflow_records.get(workflow.id)
except WorkflowNotFoundError:
raise HTTPException(status_code=404, detail="Workflow not found")
if not current_user.is_admin and existing.user_id != current_user.user_id:
raise HTTPException(status_code=403, detail="Not authorized to update this workflow")
# Pass user_id for defense-in-depth SQL scoping; admins pass None to allow any.
user_id = None if current_user.is_admin else current_user.user_id
return ApiDependencies.invoker.services.workflow_records.update(workflow=workflow, user_id=user_id)
updated = ApiDependencies.invoker.services.workflow_records.update(workflow=workflow, user_id=user_id)
ApiDependencies.invoker.services.events.emit_workflow_updated(
workflow_id=updated.workflow_id,
user_id=updated.user_id,
old_is_public=existing.is_public,
new_is_public=updated.is_public,
)
return updated


@workflows_router.delete(
Expand All @@ -88,12 +107,13 @@ async def delete_workflow(
workflow_id: str = Path(description="The workflow to delete"),
) -> None:
"""Deletes a workflow"""
try:
existing = ApiDependencies.invoker.services.workflow_records.get(workflow_id)
except WorkflowNotFoundError:
raise HTTPException(status_code=404, detail="Workflow not found")

config = ApiDependencies.invoker.services.configuration
if config.multiuser:
try:
existing = ApiDependencies.invoker.services.workflow_records.get(workflow_id)
except WorkflowNotFoundError:
raise HTTPException(status_code=404, detail="Workflow not found")
if not current_user.is_admin and existing.user_id != current_user.user_id:
raise HTTPException(status_code=403, detail="Not authorized to delete this workflow")
try:
Expand All @@ -103,6 +123,11 @@ async def delete_workflow(
pass
user_id = None if current_user.is_admin else current_user.user_id
ApiDependencies.invoker.services.workflow_records.delete(workflow_id, user_id=user_id)
ApiDependencies.invoker.services.events.emit_workflow_deleted(
workflow_id=existing.workflow_id,
user_id=existing.user_id,
is_public=existing.is_public,
)


@workflows_router.post(
Expand All @@ -121,9 +146,15 @@ async def create_workflow(
# workflows remain visible. In multiuser mode, workflows are private to the creator by default.
config = ApiDependencies.invoker.services.configuration
is_public = not config.multiuser
return ApiDependencies.invoker.services.workflow_records.create(
created = ApiDependencies.invoker.services.workflow_records.create(
workflow=workflow, user_id=current_user.user_id, is_public=is_public
)
ApiDependencies.invoker.services.events.emit_workflow_created(
workflow_id=created.workflow_id,
user_id=created.user_id,
is_public=created.is_public,
)
return created


@workflows_router.get(
Expand Down Expand Up @@ -171,16 +202,31 @@ async def list_workflows(
user_id=user_id_filter,
is_public=is_public,
)
skipped_missing_workflows = 0
for workflow in workflows.items:
try:
full_workflow = ApiDependencies.invoker.services.workflow_records.get(workflow.workflow_id)
except WorkflowNotFoundError:
skipped_missing_workflows += 1
continue
compatibility = get_workflow_call_compatibility(
workflow=full_workflow.workflow.model_dump(),
workflow_id=full_workflow.workflow_id,
services=ApiDependencies.invoker.services,
user_id=current_user.user_id,
maximum_children=ApiDependencies.invoker.services.configuration.max_queue_size,
resolve_generator_items=False,
)
workflows_with_thumbnails.append(
WorkflowRecordListItemWithThumbnailDTO(
thumbnail_url=ApiDependencies.invoker.services.workflow_thumbnails.get_url(workflow.workflow_id),
call_saved_workflow_compatibility=compatibility,
**workflow.model_dump(),
)
)
return PaginatedResults[WorkflowRecordListItemWithThumbnailDTO](
items=workflows_with_thumbnails,
total=workflows.total,
total=max(len(workflows_with_thumbnails), workflows.total - skipped_missing_workflows),
page=workflows.page,
pages=workflows.pages,
per_page=workflows.per_page,
Expand Down Expand Up @@ -312,9 +358,16 @@ async def update_workflow_is_public(
raise HTTPException(status_code=403, detail="Not authorized to update this workflow")

user_id = None if current_user.is_admin else current_user.user_id
return ApiDependencies.invoker.services.workflow_records.update_is_public(
updated = ApiDependencies.invoker.services.workflow_records.update_is_public(
workflow_id=workflow_id, is_public=is_public, user_id=user_id
)
ApiDependencies.invoker.services.events.emit_workflow_updated(
workflow_id=updated.workflow_id,
user_id=updated.user_id,
old_is_public=existing.is_public,
new_is_public=updated.is_public,
)
return updated


@workflows_router.get("/tags", operation_id="get_all_tags")
Expand Down
64 changes: 64 additions & 0 deletions invokeai/app/api/sockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@
ModelLoadStartedEvent,
QueueClearedEvent,
QueueEventBase,
QueueItemsRetriedEvent,
QueueItemStatusChangedEvent,
RecallParametersUpdatedEvent,
WorkflowCreatedEvent,
WorkflowDeletedEvent,
WorkflowEventBase,
WorkflowUpdatedEvent,
register_events,
)
from invokeai.backend.util.logging import InvokeAILogger
Expand Down Expand Up @@ -65,6 +70,7 @@ class BulkDownloadSubscriptionEvent(BaseModel):
InvocationErrorEvent,
QueueItemStatusChangedEvent,
BatchEnqueuedEvent,
QueueItemsRetriedEvent,
QueueClearedEvent,
RecallParametersUpdatedEvent,
}
Expand All @@ -86,6 +92,7 @@ class BulkDownloadSubscriptionEvent(BaseModel):
}

BULK_DOWNLOAD_EVENTS = {BulkDownloadStartedEvent, BulkDownloadCompleteEvent, BulkDownloadErrorEvent}
WORKFLOW_EVENTS = {WorkflowCreatedEvent, WorkflowUpdatedEvent, WorkflowDeletedEvent}


class SocketIO:
Expand Down Expand Up @@ -115,6 +122,7 @@ def __init__(self, app: FastAPI):
register_events(QUEUE_EVENTS, self._handle_queue_event)
register_events(MODEL_EVENTS, self._handle_model_event)
register_events(BULK_DOWNLOAD_EVENTS, self._handle_bulk_image_download_event)
register_events(WORKFLOW_EVENTS, self._handle_workflow_event)

async def _handle_connect(self, sid: str, environ: dict, auth: dict | None) -> bool:
"""Handle socket connection and authenticate the user.
Expand Down Expand Up @@ -167,6 +175,10 @@ async def _handle_connect(self, sid: str, environ: dict, auth: dict | None) -> b
logger.info(
f"Socket {sid} connected with user_id: {token_data.user_id}, is_admin: {token_data.is_admin}"
)
await self._sio.enter_room(sid, f"user:{token_data.user_id}")
await self._sio.enter_room(sid, "workflows:shared")
if token_data.is_admin:
await self._sio.enter_room(sid, "admin")
return True

# No valid token provided. In multiuser mode this is not allowed — reject
Expand All @@ -183,6 +195,9 @@ async def _handle_connect(self, sid: str, environ: dict, auth: dict | None) -> b
"is_admin": True,
}
logger.debug(f"Socket {sid} connected as system admin (single-user mode)")
await self._sio.enter_room(sid, "user:system")
await self._sio.enter_room(sid, "workflows:shared")
await self._sio.enter_room(sid, "admin")
return True

@staticmethod
Expand Down Expand Up @@ -329,6 +344,26 @@ async def _handle_queue_event(self, event: FastAPIEvent[QueueEventBase]):
await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room="admin")
logger.debug(f"Emitted private batch_enqueued event to user room {user_room} and admin room")

# QueueItemsRetriedEvent carries queue item ids that should only be visible
# to the affected owners + admins.
elif isinstance(event_data, QueueItemsRetriedEvent):
for user_id in event_data.user_ids:
user_room = f"user:{user_id}"
owner_event_data = event_data.model_copy(
update={
"retried_item_ids": event_data.retried_item_ids_by_user.get(user_id, []),
"user_ids": [user_id],
"retried_item_ids_by_user": {user_id: event_data.retried_item_ids_by_user.get(user_id, [])},
}
)
await self._sio.emit(
event=event_name, data=owner_event_data.model_dump(mode="json"), room=user_room
)
await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room="admin")
logger.debug(
f"Emitted private queue_items_retried event to user rooms {event_data.user_ids} and admin room"
)

else:
# For remaining queue events (e.g. QueueClearedEvent) that do not
# carry user identity, emit to all subscribers in the queue room.
Expand Down Expand Up @@ -360,3 +395,32 @@ async def _handle_bulk_image_download_event(self, event: FastAPIEvent[BulkDownlo
await self._sio.emit(
event=event_name, data=event_data.model_dump(mode="json"), room=event_data.bulk_download_id
)

async def _handle_workflow_event(self, event: FastAPIEvent[WorkflowEventBase]) -> None:
event_name, event_data = event
payload = event_data.model_dump(mode="json")

if not self._is_multiuser_enabled():
await self._sio.emit(event=event_name, data=payload, room="admin")
return

await self._sio.emit(event=event_name, data=payload, room=f"user:{event_data.user_id}")
await self._sio.emit(event=event_name, data=payload, room="admin")

if event_name == "workflow_created":
if getattr(event_data, "is_public", False):
await self._sio.emit(event=event_name, data=payload, room="workflows:shared")
return

if event_name == "workflow_deleted":
if getattr(event_data, "is_public", False):
await self._sio.emit(event=event_name, data=payload, room="workflows:shared")
return

if event_name == "workflow_updated":
if getattr(event_data, "new_is_public", False):
await self._sio.emit(event=event_name, data=payload, room="workflows:shared")
elif getattr(event_data, "old_is_public", False):
await self._sio.emit(
event="workflow_deleted", data={"workflow_id": event_data.workflow_id}, room="workflows:shared"
)
Loading
Loading