Resolving SIMATIC Unified HMI Script Execution via Task Scheduler

David Krause15 min read
HMI / SCADASiemensTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Problem Description

On SIMATIC Unified Comfort Panels (MTP1000, MTP1200, MTP1500, MTP1900, MTP2200 and the MTP Pro variants) and on Unified PC Runtime, custom shell scripts placed under /home/industrial/ do not execute when launched from an HMI button event or from a scheduled task. The script appears to start, the JavaScript action returns without throwing, no error is surfaced through the standard log path, and no process is visible in the runtime process list.

The most common triggering case in the field is opening a PDF stored on a connected USB medium or on the X51 storage from a button press on the panel, but the same failure mode applies to any custom .sh script that the integrator copies into the Industrial user path through the TIA Portal "User-defined scripts and files" mechanism. The behavior is identical on TIA Portal V18, V19, and V20. The V20 Update 2 image does not change it.

Symptoms in detail:

  • Button press fires the JavaScript action. The await HMIRuntime.Device.SysFct.StartProgram call resolves normally. Nothing happens on screen.
  • Scheduled task runs at the configured time. The task log marks the task as completed successfully. No shell process is spawned.
  • Direct invocation of the same script from a WinCC Unified script that runs at runtime start produces the same silent no-op.
  • Reflashing the project with the script removed and re-added does not change the behavior.

Root Cause

The Unified HMI runtime separates the privilege context of the HMI process from the context used by the task scheduler and from the context used by child processes spawned through HMIRuntime.Device.SysFct.StartProgram. The relevant distinction on a Unified Comfort Panel image is between the user that owns the WinCC Unified service and the user/group that owns /home/industrial/. The runtime scripts placed by the integrator land in the industrial user's home, and that user's group owns the directory.

Scripts copied to /home/industrial/ via TIA Portal Project > Card Reader/USB Memory > "User-defined scripts and files" or via the Control Panel's file browser lose their executable bit, or are never set executable in the first place. The TIA transfer pipeline writes the file contents but does not preserve POSIX permission bits across the image upload step. The task scheduler and the HMI JavaScript engine, when they ask the kernel to exec the script, find a file that is missing the x bit for the relevant group, and the kernel returns EACCES. The runtime swallows that error and returns success to the JavaScript layer.

There is no terminal application shipped on the Unified Comfort Panel image, and there is no shell login for the industrial user. The only way to alter file permissions on a deployed panel is to call chmod from a JavaScript action that itself uses HMIRuntime.Device.SysFct.StartProgram. Manual intervention requires either WinCC Unified Engineering on a connected engineering station with a control panel file browser that exposes permissions (uncommon) or an SSH access that is not part of the standard image.

When StartProgram is invoked with the absolute path to a non-executable script, the runtime returns no error to the JavaScript layer and the script simply does not run. The change required to fix the issue is a one-time write of the group execute bit on the file, performed before the script is invoked.

Working Solution

The pattern is a two-step sequence: first set the group execute bit on the script, then invoke the script. Both calls go through HMIRuntime.Device.SysFct.StartProgram, both use the await keyword so the JavaScript layer waits for completion before continuing, and both pass true for the blocking parameter so the call returns only when the child process exits.

try {
  // 1. Make launch.sh executable for the industrial group
  await HMIRuntime.Device.SysFct.StartProgram(
    "chmod",
    "g+x /home/industrial/launch.sh",
    0,
    true,
    undefined
  );
  // 2. Invoke the script with parameters
  let searchText = Tags("TextToSearch").Read();
  let parameters = `--find=${searchText} /media/simatic/X51/pdf_file.pdf`;
  await HMIRuntime.Device.SysFct.StartProgram(
    "/home/industrial/launch.sh",
    parameters,
    0,
    true,
    undefined
  );
} catch (err) {
  // Surface the error to an HMI tag for diagnostics
  Tags("ScriptError").Write(err.toString());
}

The first StartProgram call asks the runtime to spawn chmod g+x /home/industrial/launch.sh. The second call spawns /home/industrial/launch.sh with the constructed parameters string. Because the first call awaited completion, the second call cannot race the permission change.

The chmod call is idempotent. Once g+x is set, subsequent calls do not change the permission and do not generate an error. It is safe to run the chmod call on every button press. The cost is one extra process spawn per invocation, which on a Unified Comfort Panel is in the single-digit millisecond range.

HMIRuntime.Device.SysFct.StartProgram Parameter Reference

Index Parameter Type Value in Pattern Description
0 Program string "chmod" or "/home/industrial/launch.sh" Absolute path or executable name resolved against the runtime PATH. For built-in shell utilities use the bare name. For scripts under /home/industrial/ use the absolute path.
1 Arguments string "g+x /home/industrial/launch.sh" Single string passed to the program. Whitespace separates tokens. The string is not shell-parsed, so do not rely on glob expansion or variable substitution inside it.
2 Mode number 0 0 = start, 1 = start with confirmed wait flag, others reserved for the runtime. Use 0 for the standard pattern.
3 Wait boolean true Block the JavaScript task until the spawned process exits. Required for the chmod-then-invoke ordering to be deterministic.
4 WindowStyle number | undefined undefined Console window behavior on PC Runtime only. Pass undefined on Unified Comfort Panels to suppress any UI side effect.

Step-by-Step Implementation

  1. Open the TIA Portal project containing the Unified HMI device. Tested with TIA V19 and TIA V20 (image V20 Update 2). The same code works on TIA V18 and earlier with the same parameter set.
  2. Place launch.sh and any helper binaries under /home/industrial/ on the target panel. Use Project navigation > the Unified HMI device > "User-defined scripts and files" so the file is part of the runtime image and is restored on every download.
  3. Create a screen with a button (or a scheduled task) that fires a JavaScript action. In TIA Portal this is done by attaching a "Script" function to the "Click" event of the button or to the "On scheduled" event of a task.
  4. In the JavaScript editor, paste the two-call pattern shown above. The first call sets the execute bit. The second call invokes the actual script.
  5. If the script accepts runtime parameters, build the argument string from HMI tags using template literals. Validate any user-supplied value before concatenation to avoid command injection through the unsanitized argument string.
  6. Compile the project and download to the Unified HMI device. Confirm that the script file lands in /home/industrial/ by browsing the Control Panel file browser.
  7. Trigger the action. The first run applies the execute bit; subsequent runs are no-ops for the chmod call and a normal invocation for the script call.

Why the Task Scheduler Does Not Save You

A common false lead is to schedule a one-time task at startup that runs chmod via the Control Panel's "Run command" feature. This fails for the same reason the original script fails: the scheduled task context is a different user/group from the HMI JavaScript context, and the scheduled task cannot rely on the file having the right bits either. The task does run, but chmod itself needs the same permission infrastructure, so the workaround cascades.

Another false lead is to add the script to the /etc/init.d/ path through engineering. Unified Comfort Panels do not expose this path to TIA Portal projects; the runtime owns that directory and will not let integrator code write to it.

The supported, documented, and tested path is the JavaScript two-call pattern. It is the only method that does not require shell access to the panel and that survives a re-flash of the runtime image.

V20 Update 2 Specific Notes

V20 Update 2 of the Unified HMI image keeps the same privilege separation between the task scheduler and the HMI JavaScript engine. The chmod-then-invoke pattern continues to apply on V20 Update 2 images. Integrators who automated around the V19 behavior do not need to refactor the structure, but must keep the chmod step in the JavaScript action because file permission bits are not retained across image updates. Any project that previously relied on a one-time manual chmod via SSH must be updated to the JavaScript pattern before deployment to a panel that will receive a V20 Update 2 image.

The HMI device firmware revision that ships with the V20 Update 2 image is 20.0.0.x. The TIA Portal project version that targets it is V20 Update 2 or higher. The JavaScript runtime object model is unchanged from V19 to V20 Update 2, so the same code compiles without modification.

Verification

To confirm the script actually executed and the workaround is in place, use the following checks:

  • Inspect the Tags("ScriptError") tag you wrote into the catch block. If the value remains empty, both calls returned without throwing. The tag is visible on a diagnostics screen and can be archived through the standard WinCC Unified logging.
  • Have launch.sh write a timestamp to a file in /home/industrial/ and read that file from the HMI after a known delay. The presence of the timestamp proves the script ran.
  • On the panel, browse Control Panel > System > Process List (where the firmware exposes it) and confirm that the wrapper process is alive while the script should be running.
  • On Unified PC Runtime, use journalctl --unit simatic-unified on the host to inspect the Unified service log lines around the action time. The StartProgram call writes a line for each spawn.
  • For PDF use cases, confirm that evince opens the requested file and that the search text passed through --find is highlighted in the opened document.

A quick sanity test that does not require a deployed panel is to run the script outside the runtime with the same arguments. If the script works on the engineering station's Linux shell with the same input, the runtime is the only thing left to verify.

Diagnostic Matrix

Symptom Likely Cause Check Fix
Button does nothing, no exception in catch block Script not executable Add the chmod call before invocation Apply g+x via StartProgram as first call
Exception in catch block mentions permission Wrong path or wrong permission scope Verify the path exists, try a+x Use a+x if the script is invoked by a different user than the HMI
Script runs first time after download, fails after panel reboot Image re-flash cleared the bit Re-trigger the chmod step on every boot Add a scheduled task that calls chmod at runtime start
PDF opens but parameters are missing Single-string arguments collapsed or split Quote inside the arguments string Use --find="value" with embedded quotes or pass arguments as separate array entries
Works on Unified PC Runtime, fails on Unified Comfort Panel Group ownership differs between platforms Compare /home/industrial/ ownership on both Apply chmod a+x instead of g+x
Script runs but evince never opens Display not available to the spawned process Check DISPLAY environment and runtime session Source the runtime session environment inside the script
Second button press fails after first success Race between concurrent invocations Check for parallel actions on the same tag Wrap the two calls in a mutex tag or schedule serially

Argument Escaping and Command Injection

The StartProgram arguments string is passed to the spawned process as a single argument string. It is not shell-parsed by the runtime, which means that the process receives the literal string. The shell utilities, however, do not re-parse the string either; they receive the string as a single argv[1] and may split it themselves depending on how they are implemented. chmod with a single string argument "g+x /home/industrial/launch.sh" works because chmod accepts multiple mode/file arguments.

This is also why the pattern is dangerous when the arguments string is built from untrusted HMI tag values. A tag value of --find=test; rm -rf / concatenated into the parameters string is passed literally to launch.sh, and if the script ever evaluates the parameter in a shell context (for example, by running sh -c "$@"), the injection succeeds. The fix is to validate and whitelist any value sourced from operator input before concatenation, and to keep the script itself free of any sh -c or eval patterns.

Security Considerations

Granting execute on user-supplied scripts in /home/industrial/ is acceptable for trusted integrator code, but should not be exposed to arbitrary operator input. The argument string in StartProgram is passed to the child process as a single token and is not escaped, so any unsanitized HMI tag concatenated into the parameters becomes a command-injection vector if the child process is a shell script. Validate and whitelist any value sourced from a screen input.

Avoid setting a+x on files in shared media paths such as /media/simatic/X51/. The PDF file in the example is opened by evince as a data file, not executed, and granting world-execute on a media-mounted file is unnecessary and broadens the attack surface if the media is replaced at runtime.

For audit purposes, wrap the chmod call in a try/catch that writes the error to a HMI tag and archive that tag through the standard WinCC Unified logging. This gives a record of every successful and failed invocation and makes it possible to correlate operator actions with the JavaScript trace.

Alternative Approaches and When To Use Them

For long-running wrappers, consider placing the script under a directory the runtime preserves as executable (for example, by adding the script through a custom Yocto layer during image build). This avoids the JavaScript chmod call but requires image build infrastructure that most panel deployments do not have.

For one-off diagnostic scripts during commissioning, a USB stick with a pre-chmoded script avoids the JavaScript step entirely. Copy the script from the stick to /home/industrial/ once and the execute bit is preserved because the copy is performed by the operator's shell, not by the runtime transfer pipeline.

For recurring wrappers that need to run before any operator interaction (for example, a status daemon), add a scheduled task that runs at runtime start and that calls the chmod pattern in a JavaScript action. This combines the persistence of a scheduled task with the privilege context of the HMI JavaScript engine.

None of these alternatives replace the JavaScript pattern in production operator-triggered code, but they are useful for the first-time bring-up and for daemons that must start before the operator ever sees the screen.

Bringing the Pattern Up on a Fresh Panel

On a freshly delivered Unified Comfort Panel, the first time the JavaScript action fires the chmod call applies the bit and the script runs. The second time the operator presses the button, the chmod call is a no-op and the script runs directly. There is no warm-up procedure required beyond the project download.

If the panel is reflashed (for example, after a firmware update from V19 to V20 Update 2), the project is re-downloaded, and the /home/industrial/ directory is rewritten. The first operator action after the reflash re-applies the bit through the JavaScript action, and the behavior returns to normal. This makes the pattern self-healing across image updates.

General Methodology for Silent Runtime Failures

When a Unified HMI runtime action returns success but produces no visible effect, the diagnostic methodology is to wrap every step in its own try/catch and to write the result to an HMI tag. The pattern is identical to the one used in the example: an await on the call, a try/catch around it, and a tag write inside the catch. This is the standard pattern recommended for any WinCC Unified JavaScript action whose silent failure would otherwise be hard to attribute to a specific step. For more on this general approach to scheduled-task troubleshooting, the Microsoft Q&A thread on scheduled task environments documents the same pattern of validating the runtime context and the file permissions independently: Task Scheduler Not Working Properly - Microsoft Q&A. The principle translates directly to the Siemens runtime even though the two platforms are different.

Cross-Reference With Siemens Documentation

The JavaScript object model and the HMIRuntime.Device.SysFct namespace are documented in the TIA Portal Help under "SIMATIC Unified HMI > JavaScript runtime > Device > SysFct". The exact method signature for StartProgram and the supported values for the mode and wait parameters are listed in the WinCC Unified Programming Reference manual that ships with each TIA Portal installation. The same manual is also available on the Siemens Industry Online Support portal at support.industry.siemens.com under the entry for WinCC Unified V20 Update 2 documentation.

The semantics of the /home/industrial/ path and the transfer of user-defined scripts are documented in the TIA Portal Help under "SIMATIC Unified HMI > Project planning > User-defined scripts and files". The behavior of the file transfer pipeline with respect to POSIX permission bits is documented in the same section.

FAQ

Why does the script not run on the Unified HMI when invoked from a button?

Because the script file under /home/industrial/ lacks the execute bit. The TIA Portal "User-defined scripts and files" transfer does not preserve POSIX execute permission, and the task scheduler runs under a different user context than the HMI JavaScript engine. Run chmod via HMIRuntime.Device.SysFct.StartProgram before invoking the script.

Does the V20 Update 2 firmware fix this issue?

No. V20 Update 2 keeps the same privilege separation between the task scheduler and the HMI JavaScript runtime. The chmod-then-invoke pattern is still required on V20 Update 2 images.

Can I open a terminal on the Unified Comfort Panel to run chmod manually?

No. The Unified HMI image does not ship a terminal application, and there is no shell login for the industrial user. The only supported way to alter file permissions is through HMIRuntime.Device.SysFct.StartProgram in a JavaScript action.

What does the fourth StartProgram parameter (boolean) control?

It controls whether the JavaScript call blocks until the spawned process exits. Set it to true when the second call (your actual script) depends on the chmod having finished, and use the await keyword on the call.

Why does the example use g+x instead of a+x?

The industrial group owns /home/industrial/ on the Unified image, and the HMI runtime processes belong to that group. g+x is the minimum permission needed and avoids granting world-execute on the script. Switch to a+x only if the script is launched by a different user or by a service that is not in the industrial group.

What happens after a runtime image reflash?

The first operator action after the reflash re-applies the execute bit through the JavaScript chmod call, and the behavior returns to normal. The pattern is self-healing across image updates and does not require a manual re-chmod step.

Is the arguments string shell-parsed by the runtime?

No. The runtime passes the arguments string as a single argv[1] to the spawned process. Utilities like chmod split the string themselves, and shell scripts that re-invoke a shell with the parameter as a command string are vulnerable to command injection if the parameter is built from unsanitized HMI tag values.

Back to blog