Skip to main content

Automate it

immich-memories auto run is the single daily entry point. It looks at your library, decides which one memory is worth making today (a trip that ended last week, a birthday two days ago, last month's highlights, a year nobody cut yet), makes it, and exits. Schedule it once a day and the films arrive on their own.

immich-memories auto suggest            # what would it make today, and why
immich-memories auto run --dry-run # the decision, without the render
immich-memories auto run # decide and do it

It works on a plain NAS, and makes the same cuts you would get by hand there; a GPU or a model makes them better.

Docker: switch on the built-in timer​

In Docker the container's only process is the web UI, so the timer lives there. One setting:

advanced:
automation:
enabled: true # default false
daily_at: "09:00" # the container's local time (set TZ=)

or IMMICH_MEMORIES_AUTOMATION__ENABLED=true and IMMICH_MEMORIES_AUTOMATION__DAILY_AT=09:00 in your .env. That is the whole setup.

The UI process then runs the same auto run decision once a day, with the same lock, history, upload retry and notifications as the CLI. A container that was down at daily_at catches up when it starts; if the day's run already happened (a manual docker compose exec immich-memories immich-memories auto run counts) it waits for tomorrow. A manual run in progress holds the same lock, so the timer reports skipped instead of fighting it. /health/ready shows the timer under in_process_scheduler.

Bare metal: auto install​

immich-memories auto install --hour 9

This writes a launcher at ~/.immich-memories/bin/immich-memories-auto and schedules it: a launchd plist on macOS, a systemd user timer on Linux, a crontab line to paste anywhere else. The launcher looks immich-memories up on every fire, so an upgrade in place needs no reinstall. --uninstall removes both.

Three things a scheduled job does differently from your shell:

  • it does not inherit your environment. auto install copies PATH and the ACE-Step and torch variables it knows, nothing else; credentials belong in the config file;
  • it refuses a git worktree or a checkout behind its upstream, because a nightly job on stale code looks fine in the logs (--force overrides);
  • --config goes before auto, and the installed job keeps the resolved path.

On macOS a missed job runs when the Mac wakes; launchd does not wake it.

How it picks one memory​

Nine detectors propose candidates, hard rotation rules reject some, the rest are scored, and the top one is made. The score ranks memories against each other; it never touches which pictures go in a film. That is the editor's job, see How it chooses.

DetectorProposesScore
Yearlypast years with content, after 15 January0.8, 10 % off per year of age, floor 0.3
Birthdaya person whose birthday was 2 to 60 days ago0.75
Monthlythe latest completed month, if not made yet0.7, and it never looks further back
Activity bursta month with more than 2× the rolling 12-month average0.7 once over the threshold
Triptrips in the trailing year, 7 days after coming homeup to 0.75, by length (14 days) × pictures (200)
Person spotlightthe five most-pictured people0.6 × their share of the top person's count, floor 0.2
Multi-personpairs who appear together0.55, by estimated shared pictures up to 500, 50 minimum
On this daydates with content in 5+ years0.35, by years the same month has content, up to 10
Special daya catalogued day whose anniversary is within 3 days0.8, ×1.0 for a decade, ×0.85 for a half-decade, ×0.6 otherwise

Then, in order: a 1.2× boost for a memory that does not exist yet, recency (linear decay over 365 days from when the memory is timely, floor 0.5), content richness (up to 30 % of the score, log scale), and a same-type cooldown (0.3× for 7 days, 0.7× for 30 days). Per-type caps: 3 per type, 1 for on-this-day and special day, 2 for multi-person.

The rotation rules are hard. If every candidate is rejected the run is skipped; nothing relaxes a rule to get another video out:

  • only the latest completed month is eligible, and a monthly review cannot run twice in the same calendar month;
  • the previous category cannot repeat;
  • a category cannot appear more than twice in the last six completed automatic runs;
  • a person cannot come back if they were in either of the last two person runs.

Timing: birthdays fire 2 days after the date and trips 7 days after coming home, so the phone has uploaded. Trips need trips.homebase_latitude and trips.homebase_longitude (see Teach it your family). Special days come from the catalogue discover-days writes.

auto suggest prints the ranked list, each candidate's reason, the rule that rejected the others and anything held back. A candidate that failed twice in a row waits before it comes back (24 hours, then 3 days, then 7); one failure never counts, and a success clears it.

What auto run does, exactly​

One action per run, one memory per invocation: retry the oldest pending upload if there is one, otherwise make the top candidate. --cooldown (automation.cooldown_hours, 24) is measured from the last run's start with 30 minutes of slack, so a daily timer fires every day. --candidate KEY runs one exact memory_key from auto suggest --json; the rules still apply, --force skips only the cooldown, and a stale key fails rather than making something else.

The outcomes are skipped, dry_run, completed and failed; the first three exit 0. Quiet output is a stable JSON object with runtime as its first key. Key a wrapper on outcome: action is generation on every path.

{
"outcome": "dry_run",
"action": "generation",
"reason": "dry run",
"candidate_key": "trip:2026-07-02:2026-07-09:",
"category": "trip",
"run_id": null,
"error": null,
"output_path": null,
"recent_categories": ["monthly_review", "birthday"],
"rejections": [
{"category": "person_spotlight", "memory_key": "...", "rule": "person_in_last_two_person_runs"}
]
}

An upload that keeps failing is dropped after automation.max_delivery_attempts (5) tries, with a notification carrying the error; the video stays on disk. If the output or cache volume is running low (output.min_free_space_gb, 5 GB by default), a completed run's notification carries that warning too, and a film that would not fit at all fails the attempt with the same message before anything is rendered: this is the one place a headless cron deployment sees it, since nobody is watching a terminal. Every attempt writes its full output to automation-output/<attempt-id>.private.log under the cache (owner-readable, credentials redacted, downloadable from the Runs page). auto status shows the running code's version and commit, the timer, the last attempt, the cooldown and the live suggestion.

Check on it​

immich-memories auto status             # is the timer installed, when did it last run, what is next
immich-memories auto status --json # the same, for a script or jq
immich-memories auto history --limit 5 # the last five films it made on its own

auto history lists only completed automatic runs; a film you made with generate or the web UI is in runs list (runs). In Docker, prefix each with docker compose exec immich-memories.

Get told when it runs​

Notifications go through Apprise, so one URL per target covers ntfy, Discord, Telegram, email and more than a hundred others. They are off by default:

advanced:
notifications:
enabled: true
urls:
- "ntfy://ntfy.sh/my-topic"
on_success: true
on_failure: true
immich-memories auto test-notification

auto test-notification sends one message to every URL and says whether it went through. It ignores the cooldown that follows a failed delivery (cooldown_hours, 24), and a test that succeeds clears it. Every film then sends one: auto run, the Docker timer and a plain generate. The message carries the memory type, the outcome, the duration, the output path and, on a failure, a redacted error tail; no picture unless you set attach_thumbnail: true. The URLs hold credentials, so config show masks them and the database stores them encrypted. Every key is in the config reference.

Trigger it over HTTP​

One POST starts the decision auto run would have made, on the same detectors, rules, cooldown and history. You choose when, not what: an Immich workflow when an album fills up, a cron on another box, a phone shortcut, Home Assistant.

The app serves the route only when something can authenticate the caller, because this process holds your Immich API key.

auth.enabledserver.trigger_tokenPOST /api/trigger
offunset404: the route is not enabled
offsettoken required
onunsetlogged-in session required (browsers only)
onsettoken or session
export IMMICH_MEMORIES_SERVER__TRIGGER_TOKEN="$(openssl rand -hex 32)"

curl -X POST https://memories.example.com/api/trigger \
-H "x-api-key: $IMMICH_MEMORIES_SERVER__TRIGGER_TOKEN"

Keep the token in the environment: server is not a section that expands ${VAR}, so "${SOMETHING}" in config.yaml is those literal characters. It is compared in constant time and redacted from logs, /health and the config viewer. Send it only over HTTPS, behind the same reverse proxy as the web UI.

The answer is 202 Accepted with an attempt_id and a status_url; Authorization: Bearer <token> works too. 409 means a run is already going, and the body names it. GET the status_url for the live phase, then a final state (completed, failed, skipped, dry_run) with run_id, output_duration_seconds, delivery_status and immich_asset_id.

skipped is a normal answer: a workflow that fires on every upload mostly gets it back, and one that fires on a trip album asks for the best candidate right now, not for that album. For a specific film, use the CLI or the web UI.

Kubernetes​

deploy/kubernetes/base/job.yaml holds a one-off generate Job plus monthly and auto run CronJobs. It reads the immich-memories-secrets Secret and shares the volumes of the Kubernetes deployment:

kubectl apply -f deploy/kubernetes/base/job.yaml

A named memory on a named date​

The one thing auto cannot say is "a year in review every 15 January". A cron line (or a Kubernetes CronJob like the monthly one above) that runs generate says it:

# 15 January, 09:00: last year's review, uploaded to an album
0 9 15 1 * immich-memories generate --memory-type year_in_review --year $(( $(date +\%Y) - 1 )) --upload-to-immich --album "Memories"

The scheduler command group that used to do this is gone (Upgrading).

Every automation: and notifications: key is in the config reference, every flag in the CLI reference.