feat: workflow foundation (#23621)

* feat: plugins

* feat: table definition

* feat: type and migration

* feat: add repositories

* feat: validate manifest with class-validator and load manifest info to database

* feat: workflow/plugin controller/service layer

* feat: implement workflow logic

* feat: make trigger static

* feat: dynamical instantiate plugin instances

* fix: access control and helper script

* feat: it works

* chore: simplify

* refactor: refactor and use queue for workflow execution

* refactor: remove unsused property in plugin-schema

* build wasm in prod

* feat: plugin loader in transaction

* fix: docker build arm64

* generated files

* shell check

* fix tests

* fix: waiting for migration to finish before loading plugin

* remove context reassignment

* feat: use mise to manage extism tools (#23760)

* pr feedback

* refactor: create workflow now including create filters and actions

* feat: workflow medium tests

* fix: broken medium test

* feat: medium tests

* chore: unify workflow job

* sign user id with jwt

* chore: query plugin with filters and action

* chore: read manifest in repository

* chore: load manifest from server configs

* merge main

* feat: endpoint documentation

* pr feedback

* load plugin from absolute path

* refactor:handle trigger

* throw error and return early

* pr feedback

* unify plugin services

* fix: plugins code

* clean up

* remove triggerConfig

* clean up

* displayName and methodName

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
Co-authored-by: bo0tzz <git@bo0tzz.me>
This commit is contained in:
Alex
2025-11-14 14:05:05 -06:00
committed by GitHub
parent d784d431d0
commit 4dcc049465
89 changed files with 7264 additions and 14 deletions

View File

@@ -7865,6 +7865,111 @@
"x-immich-state": "Stable"
}
},
"/plugins": {
"get": {
"description": "Retrieve a list of plugins available to the authenticated user.",
"operationId": "getPlugins",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/PluginResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "List all plugins",
"tags": [
"Plugins"
],
"x-immich-history": [
{
"version": "v2.3.0",
"state": "Added"
},
{
"version": "v2.3.0",
"state": "Alpha"
}
],
"x-immich-permission": "plugin.read",
"x-immich-state": "Alpha"
}
},
"/plugins/{id}": {
"get": {
"description": "Retrieve information about a specific plugin by its ID.",
"operationId": "getPlugin",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PluginResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Retrieve a plugin",
"tags": [
"Plugins"
],
"x-immich-history": [
{
"version": "v2.3.0",
"state": "Added"
},
{
"version": "v2.3.0",
"state": "Alpha"
}
],
"x-immich-permission": "plugin.read",
"x-immich-state": "Alpha"
}
},
"/search/cities": {
"get": {
"description": "Retrieve a list of assets with each asset belonging to a different city. This endpoint is used on the places pages to show a single thumbnail for each city the user has assets in.",
@@ -13485,6 +13590,276 @@
],
"x-immich-state": "Stable"
}
},
"/workflows": {
"get": {
"description": "Retrieve a list of workflows available to the authenticated user.",
"operationId": "getWorkflows",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/WorkflowResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "List all workflows",
"tags": [
"Workflows"
],
"x-immich-history": [
{
"version": "v2.3.0",
"state": "Added"
},
{
"version": "v2.3.0",
"state": "Alpha"
}
],
"x-immich-permission": "workflow.read",
"x-immich-state": "Alpha"
},
"post": {
"description": "Create a new workflow, the workflow can also be created with empty filters and actions.",
"operationId": "createWorkflow",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WorkflowCreateDto"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WorkflowResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Create a workflow",
"tags": [
"Workflows"
],
"x-immich-history": [
{
"version": "v2.3.0",
"state": "Added"
},
{
"version": "v2.3.0",
"state": "Alpha"
}
],
"x-immich-permission": "workflow.create",
"x-immich-state": "Alpha"
}
},
"/workflows/{id}": {
"delete": {
"description": "Delete a workflow by its ID.",
"operationId": "deleteWorkflow",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Delete a workflow",
"tags": [
"Workflows"
],
"x-immich-history": [
{
"version": "v2.3.0",
"state": "Added"
},
{
"version": "v2.3.0",
"state": "Alpha"
}
],
"x-immich-permission": "workflow.delete",
"x-immich-state": "Alpha"
},
"get": {
"description": "Retrieve information about a specific workflow by its ID.",
"operationId": "getWorkflow",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WorkflowResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Retrieve a workflow",
"tags": [
"Workflows"
],
"x-immich-history": [
{
"version": "v2.3.0",
"state": "Added"
},
{
"version": "v2.3.0",
"state": "Alpha"
}
],
"x-immich-permission": "workflow.read",
"x-immich-state": "Alpha"
},
"put": {
"description": "Update the information of a specific workflow by its ID. This endpoint can be used to update the workflow name, description, trigger type, filters and actions order, etc.",
"operationId": "updateWorkflow",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WorkflowUpdateDto"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WorkflowResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Update a workflow",
"tags": [
"Workflows"
],
"x-immich-history": [
{
"version": "v2.3.0",
"state": "Added"
},
{
"version": "v2.3.0",
"state": "Alpha"
}
],
"x-immich-permission": "workflow.update",
"x-immich-state": "Alpha"
}
}
},
"info": {
@@ -13566,6 +13941,10 @@
"name": "People",
"description": "A person is a collection of faces, which can be favorited and named. A person can also be merged into another person. People are automatically created via the face recognition job."
},
{
"name": "Plugins",
"description": "A plugin is an installed module that makes filters and actions available for the workflow feature."
},
{
"name": "Search",
"description": "Endpoints related to searching assets via text, smart search, optical character recognition (OCR), and other filters like person, album, and other metadata. Search endpoints usually support pagination and sorting."
@@ -13621,6 +14000,10 @@
{
"name": "Views",
"description": "Endpoints for specialized views, such as the folder view."
},
{
"name": "Workflows",
"description": "A workflow is a set of actions that run whenever a triggering event occurs. Workflows also can include filters to further limit execution."
}
],
"servers": [
@@ -17022,6 +17405,10 @@
"pinCode.create",
"pinCode.update",
"pinCode.delete",
"plugin.create",
"plugin.read",
"plugin.update",
"plugin.delete",
"server.about",
"server.apkLinks",
"server.storage",
@@ -17071,6 +17458,10 @@
"userProfileImage.read",
"userProfileImage.update",
"userProfileImage.delete",
"workflow.create",
"workflow.read",
"workflow.update",
"workflow.delete",
"adminUser.create",
"adminUser.read",
"adminUser.update",
@@ -17367,6 +17758,152 @@
],
"type": "object"
},
"PluginActionResponseDto": {
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"methodName": {
"type": "string"
},
"pluginId": {
"type": "string"
},
"schema": {
"nullable": true,
"type": "object"
},
"supportedContexts": {
"items": {
"$ref": "#/components/schemas/PluginContext"
},
"type": "array"
},
"title": {
"type": "string"
}
},
"required": [
"description",
"id",
"methodName",
"pluginId",
"schema",
"supportedContexts",
"title"
],
"type": "object"
},
"PluginContext": {
"enum": [
"asset",
"album",
"person"
],
"type": "string"
},
"PluginFilterResponseDto": {
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"methodName": {
"type": "string"
},
"pluginId": {
"type": "string"
},
"schema": {
"nullable": true,
"type": "object"
},
"supportedContexts": {
"items": {
"$ref": "#/components/schemas/PluginContext"
},
"type": "array"
},
"title": {
"type": "string"
}
},
"required": [
"description",
"id",
"methodName",
"pluginId",
"schema",
"supportedContexts",
"title"
],
"type": "object"
},
"PluginResponseDto": {
"properties": {
"actions": {
"items": {
"$ref": "#/components/schemas/PluginActionResponseDto"
},
"type": "array"
},
"author": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"description": {
"type": "string"
},
"filters": {
"items": {
"$ref": "#/components/schemas/PluginFilterResponseDto"
},
"type": "array"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"title": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"actions",
"author",
"createdAt",
"description",
"filters",
"id",
"name",
"title",
"updatedAt",
"version"
],
"type": "object"
},
"PluginTriggerType": {
"enum": [
"AssetCreate",
"PersonRecognized"
],
"type": "string"
},
"PurchaseResponse": {
"properties": {
"hideBuyButtonUntil": {
@@ -17438,7 +17975,8 @@
"library",
"notifications",
"backupDatabase",
"ocr"
"ocr",
"workflow"
],
"type": "string"
},
@@ -17552,6 +18090,9 @@
},
"videoConversion": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"workflow": {
"$ref": "#/components/schemas/QueueResponseDto"
}
},
"required": [
@@ -17570,7 +18111,8 @@
"smartSearch",
"storageTemplateMigration",
"thumbnailGeneration",
"videoConversion"
"videoConversion",
"workflow"
],
"type": "object"
},
@@ -20420,6 +20962,9 @@
},
"videoConversion": {
"$ref": "#/components/schemas/JobSettingsDto"
},
"workflow": {
"$ref": "#/components/schemas/JobSettingsDto"
}
},
"required": [
@@ -20434,7 +20979,8 @@
"sidecar",
"smartSearch",
"thumbnailGeneration",
"videoConversion"
"videoConversion",
"workflow"
],
"type": "object"
},
@@ -21999,6 +22545,211 @@
"webm"
],
"type": "string"
},
"WorkflowActionItemDto": {
"properties": {
"actionConfig": {
"type": "object"
},
"actionId": {
"format": "uuid",
"type": "string"
}
},
"required": [
"actionId"
],
"type": "object"
},
"WorkflowActionResponseDto": {
"properties": {
"actionConfig": {
"nullable": true,
"type": "object"
},
"actionId": {
"type": "string"
},
"id": {
"type": "string"
},
"order": {
"type": "number"
},
"workflowId": {
"type": "string"
}
},
"required": [
"actionConfig",
"actionId",
"id",
"order",
"workflowId"
],
"type": "object"
},
"WorkflowCreateDto": {
"properties": {
"actions": {
"items": {
"$ref": "#/components/schemas/WorkflowActionItemDto"
},
"type": "array"
},
"description": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"filters": {
"items": {
"$ref": "#/components/schemas/WorkflowFilterItemDto"
},
"type": "array"
},
"name": {
"type": "string"
},
"triggerType": {
"allOf": [
{
"$ref": "#/components/schemas/PluginTriggerType"
}
]
}
},
"required": [
"actions",
"filters",
"name",
"triggerType"
],
"type": "object"
},
"WorkflowFilterItemDto": {
"properties": {
"filterConfig": {
"type": "object"
},
"filterId": {
"format": "uuid",
"type": "string"
}
},
"required": [
"filterId"
],
"type": "object"
},
"WorkflowFilterResponseDto": {
"properties": {
"filterConfig": {
"nullable": true,
"type": "object"
},
"filterId": {
"type": "string"
},
"id": {
"type": "string"
},
"order": {
"type": "number"
},
"workflowId": {
"type": "string"
}
},
"required": [
"filterConfig",
"filterId",
"id",
"order",
"workflowId"
],
"type": "object"
},
"WorkflowResponseDto": {
"properties": {
"actions": {
"items": {
"$ref": "#/components/schemas/WorkflowActionResponseDto"
},
"type": "array"
},
"createdAt": {
"type": "string"
},
"description": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"filters": {
"items": {
"$ref": "#/components/schemas/WorkflowFilterResponseDto"
},
"type": "array"
},
"id": {
"type": "string"
},
"name": {
"nullable": true,
"type": "string"
},
"ownerId": {
"type": "string"
},
"triggerType": {
"enum": [
"AssetCreate",
"PersonRecognized"
],
"type": "string"
}
},
"required": [
"actions",
"createdAt",
"description",
"enabled",
"filters",
"id",
"name",
"ownerId",
"triggerType"
],
"type": "object"
},
"WorkflowUpdateDto": {
"properties": {
"actions": {
"items": {
"$ref": "#/components/schemas/WorkflowActionItemDto"
},
"type": "array"
},
"description": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"filters": {
"items": {
"$ref": "#/components/schemas/WorkflowFilterItemDto"
},
"type": "array"
},
"name": {
"type": "string"
}
},
"type": "object"
}
}
}

View File

@@ -732,6 +732,7 @@ export type QueuesResponseDto = {
storageTemplateMigration: QueueResponseDto;
thumbnailGeneration: QueueResponseDto;
videoConversion: QueueResponseDto;
workflow: QueueResponseDto;
};
export type JobCreateDto = {
name: ManualJobName;
@@ -926,6 +927,36 @@ export type AssetFaceUpdateDto = {
export type PersonStatisticsResponseDto = {
assets: number;
};
export type PluginActionResponseDto = {
description: string;
id: string;
methodName: string;
pluginId: string;
schema: object | null;
supportedContexts: PluginContext[];
title: string;
};
export type PluginFilterResponseDto = {
description: string;
id: string;
methodName: string;
pluginId: string;
schema: object | null;
supportedContexts: PluginContext[];
title: string;
};
export type PluginResponseDto = {
actions: PluginActionResponseDto[];
author: string;
createdAt: string;
description: string;
filters: PluginFilterResponseDto[];
id: string;
name: string;
title: string;
updatedAt: string;
version: string;
};
export type SearchExploreItem = {
data: AssetResponseDto;
value: string;
@@ -1411,6 +1442,7 @@ export type SystemConfigJobDto = {
smartSearch: JobSettingsDto;
thumbnailGeneration: JobSettingsDto;
videoConversion: JobSettingsDto;
workflow: JobSettingsDto;
};
export type SystemConfigLibraryScanDto = {
cronExpression: string;
@@ -1667,6 +1699,54 @@ export type CreateProfileImageResponseDto = {
profileImagePath: string;
userId: string;
};
export type WorkflowActionResponseDto = {
actionConfig: object | null;
actionId: string;
id: string;
order: number;
workflowId: string;
};
export type WorkflowFilterResponseDto = {
filterConfig: object | null;
filterId: string;
id: string;
order: number;
workflowId: string;
};
export type WorkflowResponseDto = {
actions: WorkflowActionResponseDto[];
createdAt: string;
description: string;
enabled: boolean;
filters: WorkflowFilterResponseDto[];
id: string;
name: string | null;
ownerId: string;
triggerType: TriggerType;
};
export type WorkflowActionItemDto = {
actionConfig?: object;
actionId: string;
};
export type WorkflowFilterItemDto = {
filterConfig?: object;
filterId: string;
};
export type WorkflowCreateDto = {
actions: WorkflowActionItemDto[];
description?: string;
enabled?: boolean;
filters: WorkflowFilterItemDto[];
name: string;
triggerType: PluginTriggerType;
};
export type WorkflowUpdateDto = {
actions?: WorkflowActionItemDto[];
description?: string;
enabled?: boolean;
filters?: WorkflowFilterItemDto[];
name?: string;
};
/**
* List all activities
*/
@@ -3510,6 +3590,30 @@ export function getPersonThumbnail({ id }: {
...opts
}));
}
/**
* List all plugins
*/
export function getPlugins(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: PluginResponseDto[];
}>("/plugins", {
...opts
}));
}
/**
* Retrieve a plugin
*/
export function getPlugin({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: PluginResponseDto;
}>(`/plugins/${encodeURIComponent(id)}`, {
...opts
}));
}
/**
* Retrieve assets by city
*/
@@ -4824,6 +4928,72 @@ export function getUniqueOriginalPaths(opts?: Oazapfts.RequestOpts) {
...opts
}));
}
/**
* List all workflows
*/
export function getWorkflows(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: WorkflowResponseDto[];
}>("/workflows", {
...opts
}));
}
/**
* Create a workflow
*/
export function createWorkflow({ workflowCreateDto }: {
workflowCreateDto: WorkflowCreateDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 201;
data: WorkflowResponseDto;
}>("/workflows", oazapfts.json({
...opts,
method: "POST",
body: workflowCreateDto
})));
}
/**
* Delete a workflow
*/
export function deleteWorkflow({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/workflows/${encodeURIComponent(id)}`, {
...opts,
method: "DELETE"
}));
}
/**
* Retrieve a workflow
*/
export function getWorkflow({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: WorkflowResponseDto;
}>(`/workflows/${encodeURIComponent(id)}`, {
...opts
}));
}
/**
* Update a workflow
*/
export function updateWorkflow({ id, workflowUpdateDto }: {
id: string;
workflowUpdateDto: WorkflowUpdateDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: WorkflowResponseDto;
}>(`/workflows/${encodeURIComponent(id)}`, oazapfts.json({
...opts,
method: "PUT",
body: workflowUpdateDto
})));
}
export enum ReactionLevel {
Album = "album",
Asset = "asset"
@@ -4976,6 +5146,10 @@ export enum Permission {
PinCodeCreate = "pinCode.create",
PinCodeUpdate = "pinCode.update",
PinCodeDelete = "pinCode.delete",
PluginCreate = "plugin.create",
PluginRead = "plugin.read",
PluginUpdate = "plugin.update",
PluginDelete = "plugin.delete",
ServerAbout = "server.about",
ServerApkLinks = "server.apkLinks",
ServerStorage = "server.storage",
@@ -5025,6 +5199,10 @@ export enum Permission {
UserProfileImageRead = "userProfileImage.read",
UserProfileImageUpdate = "userProfileImage.update",
UserProfileImageDelete = "userProfileImage.delete",
WorkflowCreate = "workflow.create",
WorkflowRead = "workflow.read",
WorkflowUpdate = "workflow.update",
WorkflowDelete = "workflow.delete",
AdminUserCreate = "adminUser.create",
AdminUserRead = "adminUser.read",
AdminUserUpdate = "adminUser.update",
@@ -5083,7 +5261,8 @@ export enum QueueName {
Library = "library",
Notifications = "notifications",
BackupDatabase = "backupDatabase",
Ocr = "ocr"
Ocr = "ocr",
Workflow = "workflow"
}
export enum QueueCommand {
Start = "start",
@@ -5104,6 +5283,11 @@ export enum PartnerDirection {
SharedBy = "shared-by",
SharedWith = "shared-with"
}
export enum PluginContext {
Asset = "asset",
Album = "album",
Person = "person"
}
export enum SearchSuggestionType {
Country = "country",
State = "state",
@@ -5255,3 +5439,11 @@ export enum OAuthTokenEndpointAuthMethod {
ClientSecretPost = "client_secret_post",
ClientSecretBasic = "client_secret_basic"
}
export enum TriggerType {
AssetCreate = "AssetCreate",
PersonRecognized = "PersonRecognized"
}
export enum PluginTriggerType {
AssetCreate = "AssetCreate",
PersonRecognized = "PersonRecognized"
}