Trigger a systemd Service When a Directory Gets Files
You will create a systemd.path unit that watches /var/lib/incoming and starts a matching service when that directory is non-empty. The service in this example writes a journal message, so you can verify the trigger without inventing a real file-processing job. Allow about fifteen minutes if you have administrator access and a test machine.
The route
Jump straight to the step you need, or tick off Done means at the end.
The installed package here is systemd 255.4-1ubuntu8.17, providing systemd 255. The local manual describes the behaviour used below. A path unit is a configuration file, not a command that polls in a loop: systemd uses Linux inotify, so changes made on a remote NFS file system are not suitable for this pattern.
1. Choose the path and matching unit names
Pick one absolute directory and give the path unit and service the same base name. The file incoming-watch.path will activate incoming-watch.service by default. You can override that relationship with Unit=, but keeping the names aligned makes the deployment easier to inspect.
This example uses a system-wide directory, so the file creation and service-management commands require 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 useful checkpoint is that the major version is 255 or newer than the version whose manual you are following.
2. Create a harmless test service
First create the directory, then install a oneshot service. The service does not consume or delete files. 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 runs logger and exits; replace ExecStart with your real processor only after you have decided which user, permissions and failure handling it needs. Do not put a shell pipeline into ExecStart unless you explicitly need a shell and have quoted its inputs.
3. Add the path unit
Use DirectoryNotEmpty= when the condition is the presence of at least one file in a directory. It also starts the configured service immediately when the directory is already non-empty as the path unit becomes active. That startup behaviour is useful for a queue, but it can surprise you 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 required here because the default is the matching service name, but it documents the link for someone reading the file later. The path argument must be absolute. WantedBy=paths.target is used by systemctl enable; it does not start the watcher by itself.
The two trigger-limit settings are systemd 255 options added in version 250. The defaults are a two-second interval and 200 permitted activations. If the limit is reached, the path unit fails and stops watching until it is restarted. Keep the limit in place for a service that could trigger itself or repeatedly fail. Setting either value to zero disables trigger rate limiting, which is 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. These commands change systemd's in-memory view but do not yet enable or start the watcher:
$ 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. If it reports an unknown executable, check the path in ExecStart with command -v logger and use the absolute result. If it reports a unit-name or section error, fix that before starting anything.
Checkpoint: make sure the directory is empty if you want to observe a new file causing the first activation:
$ sudo find /var/lib/incoming -mindepth 1 -maxdepth 1 -print
This only lists entries. Do not add a broad rm command to clear a production queue. If the directory already contains useful files, leave them in place and expect the path unit to start the service as soon as it is activated.
5. Start the watcher and test one trigger
Start the path unit now and enable it for normal boot. Enabling changes a symlink under systemd's configuration, and starting it begins monitoring the directory. Do this during a suitable 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 the useful result. Now create one test file. This changes state in the watched directory, so use an obviously disposable name:
$ 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 contains host-specific identifiers and timestamps. The path unit normally remains active after triggering the service. Read the service journal to confirm the test action:
$ sudo journalctl -u incoming-watch.service -n 5 --no-pager
... incoming-watch[...] : incoming directory is non-empty
The timestamp and process identifier are variable. If the service ran, the path-to-service connection works. Because the directory is still non-empty, creating another file may cause another activation, subject to the trigger limit.
6. Understand the event choices
DirectoryNotEmpty= is a condition, not a per-file queue protocol. A path unit can activate a service again after that service terminates while the condition remains satisfied. A service that leaves the directory non-empty can therefore be started repeatedly. Make the service idempotent, move or remove successfully processed files deliberately, or use a separate queue design that handles retries.
For other jobs, choose the directive that matches the event you actually need:
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 after 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=. They do not cause an equivalent immediate activation for PathChanged= or PathModified=. Multiple path directives can be combined, 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 are a relative path, a directory that was not created, a service executable that does not exist, and expecting PathChanged= to behave like DirectoryNotEmpty=. If a start-rate limit was hit, correct 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, run:
$ 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
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 if you may need to restore it.
Done means
- The path and service use absolute paths and a clear matching unit name.
systemd-analyze verifyaccepted both unit files.- The watcher is active and, if required, enabled for boot.
- A disposable file produced a journal entry from the matching service.
- The service is safe to run again while its condition remains true.
- The trigger limit, failure path and undo commands are known before using real data.