Home / Alt manpages / systemd.path(5)

  • systemd.path(5)
  • File format
  • linux

Watch a Directory and Trigger a Service with systemd.path

A systemd.path unit watches a directory with inotify and starts a matching service the moment it stops being empty, no polling loop required. This walks through building and testing one on /var/lib/incoming, with a harmless logging service standing in for your real job. Allow about fifteen minutes if you have administrator access and a test machine.

The installed package here is systemd 255.4-1ubuntu8.17, providing systemd 255. A path unit is a configuration file, not a command that polls: since it relies on Linux inotify, changes made on a remote NFS filesystem will not trigger it reliably.

1. Choose the path and matching unit names

Pick one absolute directory and give the path unit and service the same base name. incoming-watch.path will activate incoming-watch.service by default. You can override that with Unit=, but matching names keeps the deployment easy to inspect later.

This example uses a system-wide directory, so creating files and managing the service need sudo. Reading unit files and checking the installed version do not:

$ systemd --version
systemd 255 (255.4-1ubuntu8.17 ...)
$ command -v systemctl
/usr/bin/systemctl

The exact feature list after the version varies by package build. The checkpoint that matters is the major version: 255 or newer than the manual you are following.

2. Create a harmless test service

Create the directory, then install a oneshot service that does not consume or delete anything. It only records that the path unit started it, which keeps the test reversible and makes repeated triggers visible in the journal.

$ sudo install -d -m 0755 /var/lib/incoming
$ sudo tee /etc/systemd/system/incoming-watch.service >/dev/null <<'EOF'
[Unit]
Description=Test action for the incoming directory

[Service]
Type=oneshot
ExecStart=/usr/bin/logger -t incoming-watch incoming directory is non-empty
EOF

install -d needs elevated privileges because it creates a directory below /var. The service just runs logger and exits; replace ExecStart with your real processor only once you have decided which user, permissions and failure handling it needs. Do not put a shell pipeline into ExecStart unless you actually need a shell, and have quoted its inputs.

3. Add the path unit

Use DirectoryNotEmpty= when the condition is "at least one file present". It also fires the configured service immediately if the directory is already non-empty when the path unit becomes active, useful for a queue but easy to be surprised by during deployment.

$ sudo tee /etc/systemd/system/incoming-watch.path >/dev/null <<'EOF'
[Unit]
Description=Watch the incoming directory

[Path]
DirectoryNotEmpty=/var/lib/incoming
Unit=incoming-watch.service
TriggerLimitIntervalSec=2s
TriggerLimitBurst=200

[Install]
WantedBy=paths.target
EOF

The explicit Unit= is not strictly required, since the default is the matching service name, but it documents the link for whoever reads the file next. The path argument must be absolute. WantedBy=paths.target is what systemctl enable uses; it does not start the watcher by itself.

Warning

The two trigger-limit settings (added in systemd 250) default to a two-second interval and 200 permitted activations. Hit the limit and the path unit fails and stops watching until restarted, so keep it in place for anything that could trigger itself or repeatedly fail. Setting either value to zero disables rate limiting entirely; treat that as a deliberate safety decision, not a routine fix.

4. Check the files before activation

Reload systemd's unit-file view, then ask it to parse both files. Neither command enables or starts the watcher yet, only changes systemd's in-memory view:

$ sudo systemctl daemon-reload
$ systemd-analyze verify /etc/systemd/system/incoming-watch.path /etc/systemd/system/incoming-watch.service

A successful verification prints nothing and returns status 0. An unknown-executable complaint means checking the path in ExecStart with command -v logger and using the absolute result. Fix any unit-name or section error before starting anything.

Checkpoint

Make sure the directory is empty first if you want to watch a new file cause the first activation:

$ sudo find /var/lib/incoming -mindepth 1 -maxdepth 1 -print

This only lists entries; do not add a broad rm to clear a production queue. If the directory already holds useful files, leave them alone and expect the path unit to start the service the moment it is activated.

5. Start the watcher and test one trigger

Start the path unit and enable it for boot. Enabling changes a symlink under systemd's configuration; starting begins monitoring the directory. Do this in a maintenance window if the real service has side effects.

$ sudo systemctl enable --now incoming-watch.path
Created symlink .../paths.target.wants/incoming-watch.path ...
$ systemctl is-enabled incoming-watch.path
enabled
$ systemctl is-active incoming-watch.path
active

The symlink path and any warning text vary by distribution; the two status words are what matters. Now create one test file, an obviously disposable name since it changes state in the watched directory:

$ printf '%s\n' 'test input' | sudo tee /var/lib/incoming/test-input.txt >/dev/null
$ systemctl status incoming-watch.path --no-pager
● incoming-watch.path ... active (running)

status output carries host-specific identifiers and timestamps. The path unit normally stays active after triggering the service. Check the service journal to confirm the test action actually ran:

$ sudo journalctl -u incoming-watch.service -n 5 --no-pager
... incoming-watch[...] : incoming directory is non-empty

Timestamp and process ID will vary; if the service ran, the path-to-service connection works. Because the directory is still non-empty, dropping another file may trigger it again, subject to the trigger limit.

6. Understand the event choices

DirectoryNotEmpty= is a condition, not a per-file queue protocol. A path unit can start the service again after it exits while the condition still holds, so a service that leaves the directory non-empty can be started repeatedly. Make the service idempotent, move or remove processed files deliberately, or use a separate queue design that handles retries.

For other jobs, pick the directive that matches what you actually need to watch:

  • PathExists= activates when a particular file or directory exists.
  • PathExistsGlob= activates when at least one path matches a glob.
  • PathChanged= notices a file or directory once a write-open file is closed.
  • PathModified= also reacts to simple writes to the watched file.

Existing paths trigger immediately for PathExists=, PathExistsGlob= and DirectoryNotEmpty=, but not for PathChanged= or PathModified=. You can combine multiple path directives, and assigning an empty value resets the paths accumulated for that option.

7. Diagnose and undo the test

If the path unit is inactive, inspect both units and the recent service log:

$ systemctl status incoming-watch.path incoming-watch.service --no-pager
$ sudo journalctl -u incoming-watch.path -u incoming-watch.service -b --no-pager

Common traps: a relative path, a directory that was never created, a service executable that does not exist, or expecting PathChanged= to behave like DirectoryNotEmpty=. If a start-rate limit was hit, fix the loop or service failure first, then restart the path unit:

$ sudo systemctl restart incoming-watch.path
$ systemctl is-active incoming-watch.path
active

To stop the test and remove its boot-time activation:

$ sudo systemctl disable --now incoming-watch.path
disabled

That leaves the unit files in place but stops monitoring. Remove the test file only if it is disposable:

$ sudo rm -- /var/lib/incoming/test-input.txt

Warning

Deleting files is irreversible; do not remove real queue entries as part of cleanup. To remove the test configuration itself, delete the two unit files during a controlled maintenance window, then run sudo systemctl daemon-reload. Keep a copy in case you need to restore it.

Done means

  • Names match. The path and service use absolute paths and a clear matching unit name.
  • Files verified. systemd-analyze verify accepted both unit files.
  • Watcher running. It is active and, if required, enabled for boot.
  • Trigger proven. A disposable file produced a journal entry from the matching service.
  • Repeat-safe. The service is safe to run again while its condition remains true.
  • Escape plan known. The trigger limit, failure path and undo commands are understood before real data touches it.