Run One-Off Commands with systemd-run

systemd-run launches an ordinary command as a transient service, scope or timer without you writing a unit file first. You will run a short command and get a reliable exit status back, then move on to scopes and scheduled timers. Allow about 15 minutes.

1. Check the installed command

Confirm which systemd-run will execute and which manager version it belongs to.

$ command -v systemd-run
/usr/bin/systemd-run
$ systemd-run --version
systemd 255

The exact feature string and distribution suffix can differ. This guide uses the installed 255 behaviour, including the default environment expansion rules covered in step 5.

Checkpoint: Continue only if the command is present and its version is close enough to the documentation you are using.

2. Run a short command and get its status back

$ systemd-run --user --wait --collect --service-type=exec \
    /bin/sh -c 'printf "%s\n" "transient service finished"'
Running as unit: run-EXAMPLE.service
transient service finished
Finished with result: success
Main processes terminated: code=exited/status=0

The generated unit name is different every run, so the final status line is the check that matters. With --wait, systemd-run propagates the command's result. --collect tells the manager to unload the transient unit after completion, including after failure, so it does not sit around loaded for later inspection.

Tip: On a machine without a usable user manager, only drop --user if you deliberately want the system manager and have the privilege for it. The implied default is --system; do not fall into it by accident.

3. Tell scopes and services apart

A scope keeps systemd-run as the parent process: the command inherits your caller's execution environment, the manager still groups and manages the processes, but the call is synchronous by default and returns when the command finishes. Scope units have no single main process, so an unclean child exit does not by itself create a service-style failure state.

$ systemd-run --user --scope /bin/sh -c 'printf "scope-ok\n"'
Running scope as unit: run-EXAMPLE.scope
scope-ok

4. Set properties without writing a unit file

Transient units accept the same property-assignment style as systemctl set-property. Give a short-lived scope a maximum runtime, for example.

$ systemd-run --user --scope \
    --property=RuntimeMaxSec=30s \
    /bin/sh -c 'printf "work started\n"; sleep 2; printf "work finished\n"'

RuntimeMaxSec= is a scope setting; the default is no runtime limit. If the process runs past it, the manager terminates the scope and puts it in a failure state. For a service, --property=Type=exec is often the first reliability improvement, since it makes the service fail when its executable cannot start.

Other practical options: --working-directory=/path/to/work, repeated --setenv=NAME=value, --uid=NAME and --gid=NAME.

Warning: Changing identity can remove access rather than add it. Test with a harmless command first, and do not assume a user service has the same environment as your interactive shell.

5. Avoid the environment-expansion trap

Two parsers touch your command text: your shell and systemd. In systemd 255, environment expansion is on by default for services but off by default for scopes, for backwards compatibility. A literal dollar sign meant for your shell can get expanded by systemd instead, or need protecting from both layers at once.

When the command contains dollar characters that systemd must not touch, disable its expansion explicitly.

$ systemd-run --user --wait --collect \
    --expand-environment=no --service-type=exec \
    /bin/sh -c 'printf "shell=%s pid=%s\n" "$SHELL" "$$"'

Single quotes protect the command text from the invoking shell. --expand-environment=no then passes the text through untouched to the shell started by the transient service. Without that option, systemd's own ${VARIABLE} expansion can change the argument before the shell ever sees it. You can instead escape a dollar sign for systemd as $$, but the explicit option is easier to review when a whole script is being passed as one argument.

6. Schedule a command with a transient timer

Timer options create a transient timer alongside the service it triggers. --on-active=5m and similar monotonic options count from timer activation; --on-calendar= uses wall-clock calendar expressions. The command does not run immediately: systemd-run starts the timer unit and reports both names.

$ systemd-run --user --on-active=5m \
    --timer-property=AccuracySec=1s \
    /usr/bin/logger --tag transient-demo 'scheduled check'
Running as unit: run-EXAMPLE.timer
Will run service as unit: run-EXAMPLE.service

AccuracySec= defaults to one minute. Setting it to one second narrows the scheduling window for this demonstration, but it does not override every clock or scheduler behaviour. For routine jobs, leave the default unless precision has a real operational benefit.

Record the actual timer name from your output, then inspect it.

$ systemctl --user status run-EXAMPLE.timer
$ journalctl --user -u run-EXAMPLE.service

Replace run-EXAMPLE with the name systemd-run printed. Stop an unwanted pending timer before it fires.

$ systemctl --user stop run-EXAMPLE.timer

For a recurring calendar schedule, use an expression such as --on-calendar='Mon..Fri 09:00'. If a missed calendar run must be caught up after shutdown, that is a persistent timer concern; the transient command-line shortcut is no substitute for a reviewed unit definition with Persistent=yes.

7. Diagnose failures without guessing

$ systemctl --user list-units --type=service --all 'run-*'
$ journalctl --user -u run-EXAMPLE.service --no-pager

A permission error usually means the selected manager, user, identity or working directory is wrong. A timer that appears late may be within AccuracySec= or affected by system suspend. A command that cannot find an executable may be missing the environment you expected: use an absolute path and set only the variables it needs.

Warning: Stopping a unit sends termination signals to its managed processes. Before running systemctl stop on a real job, confirm the unit name with systemctl status and make sure terminating it will not interrupt data writes or a deployment.

Done means