feat: workflow asset tag trigger/filter/action (#29043)

* feat: workflow asset tag trigger and filter

* feat(web): show tag names in workflow editor

* fix(web): tag picker in schema config editor

* fix: invalid plugin manifest

* chore: update workflow method wrapper type

* chore: update tag filter method declaration

* feat: workflow action to add tags to assets
This commit is contained in:
Ben Beckford 2026-08-12 16:26:03 -07:00 committed by GitHub
parent b82d480552
commit a939561e70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 175 additions and 8 deletions

View file

@ -308,6 +308,56 @@
},
"uiHints": ["Filter"]
},
{
"name": "assetAddTags",
"title": "Add Tags",
"description": "Add tags to an asset",
"types": ["AssetV1"],
"hostFunctions": true,
"schema": {
"type": "object",
"properties": {
"tags": {
"title": "Tags",
"description": "Tags to add to the asset",
"type": "string",
"array": true,
"uiHint": {
"type": "TagId"
}
}
},
"required": ["tags"]
}
},
{
"name": "assetTagFilter",
"title": "Filter by tags",
"description": "Filter assets by which tags they have",
"types": ["AssetV1"],
"schema": {
"type": "object",
"properties": {
"tags": {
"title": "Tags",
"description": "Which tags the asset must have",
"type": "string",
"array": true,
"uiHint": {
"type": "TagId"
}
},
"matching": {
"title": "Matching",
"description": "Whether assets must have all, any, or none of the listed tags",
"type": "string",
"enum": ["all", "any", "none"]
}
},
"required": ["tags", "matching"]
},
"uiHints": ["Filter"]
},
{
"name": "assetExifFilter",
"title": "Filter by EXIF metadata",

View file

@ -39,6 +39,11 @@ const matchValueResult = (value: string, config: MatchValueConfig) => {
};
const methods = wrapper<Manifest>({
assetAddTags: ({ config, data, functions }) => {
functions.bulkTagAssets({ assetIds: [data.asset.id], tagIds: config.tags });
return {};
},
assetAddToAlbums: ({ config, data, functions }) => {
const assetId = data.asset.id;
@ -176,6 +181,24 @@ const methods = wrapper<Manifest>({
return { workflow: { continue: hasTimeZone === needsTimeZone } };
},
assetTagFilter: ({ config, data }) => {
const assetTags = data.asset.tags.map((tag) => tag.id);
for (const tag of config.tags) {
if (assetTags.includes(tag)) {
if (config.matching === 'any') {
break;
} else if (config.matching === 'none') {
return { workflow: { continue: false } };
}
} else if (config.matching === 'all') {
return { workflow: { continue: false } };
}
}
return { workflow: { continue: true } };
},
assetTypeFilter: ({ config, data }) => {
return { workflow: { continue: config.allowedTypes.includes(data.asset.type) } };
},
@ -208,6 +231,7 @@ const methods = wrapper<Manifest>({
});
const {
assetAddTags,
assetAddToAlbums,
assetArchive,
assetFavorite,
@ -217,6 +241,7 @@ const {
assetDateFilter,
assetLock,
assetMissingTimeZoneFilter,
assetTagFilter,
assetTypeFilter,
assetVisibility,
webhook,
@ -226,6 +251,7 @@ const {
} = methods;
export {
assetAddTags,
assetAddToAlbums,
assetArchive,
assetFavorite,
@ -235,6 +261,7 @@ export {
assetDateFilter,
assetLock,
assetMissingTimeZoneFilter,
assetTagFilter,
assetTypeFilter,
assetVisibility,
webhook,

View file

@ -4,6 +4,8 @@ import {
type BulkIdResponseDto,
type BulkIdsDto,
type CreateAlbumDto,
type TagBulkAssetsDto,
type TagBulkAssetsResponseDto,
} from '@immich/sdk';
declare module 'extism:host' {
@ -47,6 +49,7 @@ export const availableFunctions = [
'addAssetsToAlbum',
'addAssetsToAlbums',
'httpRequest',
'bulkTagAssets',
] as const;
export const hostFunctions = (authToken: string) => {
@ -96,5 +99,11 @@ export const hostFunctions = (authToken: string) => {
authToken,
[url, options],
),
bulkTagAssets: (dto: TagBulkAssetsDto) =>
call<[TagBulkAssetsDto], TagBulkAssetsResponseDto>(
'bulkTagAssets',
authToken,
[dto],
),
} satisfies Record<(typeof availableFunctions)[number], unknown>;
};

View file

@ -1,4 +1,9 @@
import type { AssetTypeEnum, AssetVisibility, WorkflowType } from '@immich/sdk';
import type {
AssetTypeEnum,
AssetVisibility,
TagResponseDto,
WorkflowType,
} from '@immich/sdk';
type DeepPartial<T> = T extends Date
? T
@ -18,6 +23,7 @@ export type WorkflowEventData<T extends WorkflowType> = WorkflowEventMap[T];
export enum WorkflowTrigger {
AssetCreate = 'AssetCreate',
AssetMetadataExtraction = 'AssetMetadataExtraction',
AssetTagged = 'AssetTagged',
// PersonRecognized = 'PersonRecognized',
}
@ -88,6 +94,7 @@ export type AssetV1 = {
duplicateId: string | null;
visibility: AssetVisibility;
isEdited: boolean;
tags: TagResponseDto[];
exifInfo: {
make: string | null;
model: string | null;

View file

@ -7423,7 +7423,8 @@ export enum WorkflowType {
}
export enum WorkflowTrigger {
AssetCreate = "AssetCreate",
AssetMetadataExtraction = "AssetMetadataExtraction"
AssetMetadataExtraction = "AssetMetadataExtraction",
AssetTagged = "AssetTagged"
}
export enum QueueJobStatus {
Active = "active",