- v3.2.0 renamed asset_face.personId to personGroupId and dropped person.id
in favour of a (ownerId, personGroupId) key, breaking every face/person
query.
The local tag table can lag behind Immich's own tag state (e.g. tags
created directly in Immich, or before this app's sync ran), causing
remove_tag to silently no-op on a tag that actually exists. Mirror the
sibling "tag" action's approach and resolve against the Immich API,
matching on the tag's full-path "value" field. Drops the now-unused
db/sql local lookup.
The id list was built via raw string interpolation into sql.raw().
Switch to sql.join with proper parameter binding, matching the
pattern used by the other match branches in this function.
Removes a tag (by name, matching the tag's full path value) from the
matched assets via DELETE /tags/:id/assets. The tag is looked up
without creating it — removing a tag that doesn't exist is a no-op
rather than an error, so add/remove pairs are safe in the same
workflow.
Registered in the node palette, action node, run-detail view, and
action config (same tag-name field as Add Tag).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds condition types common in smart-album systems of other photo
managers (Apple Photos, Lightroom, Google Photos):
- File Size: min/max in MB (fractional) over asset_exif.fileSizeInByte
- File Name / Path: contains / not contains / starts with / ends with
against originalFileName or originalPath (ILIKE, wildcards escaped so
user text matches literally)
- File Extension: is any of / is none of a comma-separated list,
case-insensitive, matched after the last dot
- Face Count: min/max visible, non-deleted detected faces — 'how many
people', complementing the person condition's 'who'
- Time of Day: from/to hour over localDateTime (capture wall-clock
time, no timezone math); From > To wraps past midnight (22 to 5)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a "Resolution" condition type filtering assets by image
dimensions, useful for isolating low-resolution images for cleanup or
selecting high-resolution ones.
Three metrics with optional min/max bounds:
- Megapixels (width x height / 1e6, fractional values allowed)
- Short edge in pixels (LEAST of width/height)
- Long edge in pixels (GREATEST of width/height)
Short/long edge are orientation-agnostic, so a 4000x3000 landscape and
a 3000x4000 portrait are treated identically. Uses the exifImageWidth/
exifImageHeight columns on asset_exif, which the condition queries
already join; assets without recorded dimensions never match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a "Tag" condition type that filters assets by Immich tags, with
the same match modes as the person condition (has any of / has all of /
has none of). Selected tags also match their child tags via Immich's
tag_closure hierarchy table, mirroring how tag browsing behaves in
Immich itself.
- conditionBuilder: tag case with EXISTS subqueries on tag_asset,
descendant-inclusive via tag_closure (plus a direct id check so a
missing self-closure row can't drop matches)
- ConditionEditor: Tag entry + TagPicker multi-select (PersonPicker
pattern) with tag color chips
- conditionSummary: node label ("Tag any of: ...")
- new GET /api/tags route + client handler + drizzle schema for
Immich's tag table
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All workflow trigger queries were capped at .limit(10000) with no
ORDER BY and no pagination, so on libraries larger than 10k assets an
all_assets ("full library scan") trigger silently evaluated an
arbitrary 10k subset. The IF/SWITCH condition queries had the same cap,
and bulk actions sent every asset id in a single Immich API request.
- resolveAssetTrigger: keyset pagination (ORDER BY id, batches of 5000)
for all three trigger types
- IF/SWITCH: evaluate conditions against the incoming asset set in
chunks instead of one capped inArray query
- actionExecutor: call Immich bulk endpoints in batches of 1000 ids
Verified on a 26,063-asset library (Immich v3.0.1): all_assets now
resolves 26,063 (previously 10,000), a person-filter IF matched 813
assets vs 142 under the cap, and a 25,250-asset remove_from_album
branch completed via batched requests.
Fixes#296.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Updated albumColumns and AlbumThumbnail components to display placeholder when no thumbnail is available.
- added album_user table (album ownership moved here, role='owner')
- drop asset.deviceId/deviceAssetId and albums.ownerId
- asset.duration varchar -> integer (milliseconds)
- removed isArchived instead use visibility
- Add ownerId to settings table for per-user key-value storage
- Add settings KV API endpoint scoped by user
- Add workflow API key config in settings page
- Action executor uses per-user workflow API key, falls back to session auth
- Bootstrap cron scheduler on server start via instrumentation hook
- Re-register/unregister cron on workflow enable/disable/delete
- Downgrade node-cron to v3 for Turbopack compatibility
- Add structured console logs to workflow engine for all steps
- Enabled toggle saves immediately to server
- Schedule popover saves immediately to server
- Use react-hot-toast instead of shadcn toast in workflow editor
- Visual node-based workflow editor using React Flow
- Asset triggers: new asset, asset updated, all assets (with lookback buffer)
- Logic nodes: if/switch with combinable conditions (person, location, date, camera, EXIF, favorites, album membership)
- Action nodes: create album, add/remove from album, favorite, unfavorite, archive, tag
- Workflow-level triggers: manual run, cron schedule, webhook
- Debug mode: dry run with step-by-step logs and asset previews
- Run history with per-node asset tracking
- Run detail page with annotated read-only graph
- Processed asset tracking to prevent duplicate processing
- Multi-person picker with face thumbnails
- Album picker dropdown for actions
- JSON import/export for workflow sharing
- Workflow list page with create, delete, enable/disable