RMM · By · 9 min read

Software policies: desired state, not one-off installs

A software policy says what belongs on a machine. The agent re-checks on an interval, fixes what drifted, and verifies the fix took. Why that beats an install script.

Somewhere in most fleets there is a script that installed something in 2023. It ran, it exited zero, someone ticked a box, and nobody has thought about it since. Whether the thing it installed is still there is genuinely unknown. It probably is, on most machines. On a few it was uninstalled by a user who needed the disk space, or wiped by a reimage that predated the script, or upgraded into a version that broke the thing that depended on it.

You find out during an audit, or when something fails.

The problem with an install script is not that it is a script. It is that running it is an event, and what you actually want is a condition. “7-Zip got installed on Tuesday” is a much weaker statement than “7-Zip is on this machine”, and only one of them is worth anything three months later.

A policy describes the machine you want

The software policy editor has a field called State, and everything else follows from it.

The LynxTrac policy editor showing a policy named 7-Zip policy, with State set to Absent, Catalog set to 7-Zip, Package set to winget 26.0.0 for Windows 11, and an Enforce Interval of 1 Hour.
State is an editable field with two values, sitting next to the interval that decides how often it gets checked. This policy is open mid-edit showing Absent; the enforcement run further down happened while it was set to Present. One loop serves both directions.

You pick a catalog entry, pick the package that satisfies it, and declare whether that software should be present or absent. Then you set an interval. This policy is set to one hour, so once an hour the agent asks whether reality matches the sentence, and does something about it when it does not.

Nothing about that is exotic. Configuration management has worked this way for twenty years. What is worth pointing at is that it applies to ordinary endpoint software here, on the same agent already doing monitoring and patching, without a separate tool and a separate language to learn.

Policies bind to targets separately from how they are defined, so the same “7-Zip should be present” rule can apply to a whole tenant or to a single device, and you can see at a glance which policies are approved and active before any of them touch a machine.

The loop leaves a trail you can read backwards

What surprised me was the event stream. Every evaluation writes its own history, and reading it bottom to top is the clearest explanation of the model I have seen.

The software audit history table listing ten events for the 7-Zip policy, newest first: Install Succeeded, Running, Dispatched, Queued, Job Created, Package Selected, Policy Eval Start, then Uninstall Succeeded, Running and Dispatched from the previous cycle.
One enforcement cycle, newest at the top. Read up from Policy Eval Start: the policy is evaluated, a package is chosen, a job is created and queued, the agent acknowledges it, the work runs, and the outcome is recorded. The three rows at the bottom are the tail of the previous cycle running the other direction.

Start at Policy Eval Start, seven rows down. Its message reads Evaluating policy "7-Zip policy" (present), which names the desired state in the evaluation itself. Then Package Selected picks the specific package for the catalog. Job Created makes the work item. Queued hands it off. Dispatched is the agent saying it received the command, which is a distinct thing from the command working. Running is the work. Install Succeeded is the outcome.

Seven events for one install is a lot of bookkeeping. It pays for itself the first time something sticks. A job that reached Dispatched and stopped is an agent problem. A job that reached Running and failed is a package problem. Those get diagnosed differently and you can tell them apart without opening anything.

The step nobody usually builds

Open one of those completed jobs and you get the receipt.

The job history detail for job LSJ379: action install, status succeeded, duration 13.1 seconds, retry count 0. A reason line explains that 7-Zip is not installed and version 26.0.0 is being installed. Step 1 shows the full winget install command, exit code 0, and the installer's standard output. A separate Post-check step ran winget list and printed a table confirming 7-Zip 26.02 is installed.
The Post-check block at the bottom is the part worth noticing. After the install exits, a second command runs winget list against the same package ID and captures what comes back. Succeeded means the software was found afterwards, not that the installer did not complain.

Working down: a reason line saying why this job exists at all, which here is that 7-Zip was not installed and version 26.0.0 is going on. The exact command, in full, including every flag. The installer’s own standard output, unedited, down to the licensing notice and the URL it pulled the MSI from. An exit code and a duration.

Then Post-check, which runs winget list --id=7zip.7zip and prints the table that comes back.

That last step is the difference between an install job and a working one. Installers lie. They exit zero after failing to write a file, they succeed into a pending-reboot state, they report success for a package that a policy on the machine then quietly blocks. An exit code tells you the installer finished. Asking the package manager what is actually on the box afterwards tells you whether it worked, and it costs about a second.

There is a retry count on the same panel, so a job that fails transiently is retried rather than sitting in a failed list waiting for a human to notice.

Fleets are not uniform and the catalog admits it

The other thing that survives contact with reality is the package catalog.

The software package catalog listing ten entries with columns for Name, Platform, Distro, Arch, Version and Type. 7-Zip appears four times with types Script, MSI, EXE and Scoop. Other rows show Notepad++ as Winget, Spotify as MS Store, GitHub as MSI, Python as Winget and Postman as Chocolatey.
7-Zip appears four times in this list, as Script, MSI, EXE and Scoop. Same software, four ways of getting it onto a machine, because the machines are not all the same.

7-Zip is in there four times. Once as a Script, once as an MSI, once as an EXE, once via Scoop. Elsewhere in the same catalog there are Winget packages, an MS Store app and a Chocolatey package. Every row records its own platform, distribution, architecture and version, and a policy points at a catalog entry rather than at a package directly.

This looks like clutter until you have a fleet with a locked-down subnet where Winget cannot reach its source, a handful of developer machines that already have Scoop, and a set of servers where you would rather hand the MSI over to the installer yourself. One policy, several packages that can satisfy it, and the right one gets chosen per machine.

When two candidates tie on version, the choice is not left to whichever row the database returns first. There is a fixed preference order per platform, so the same policy resolves to the same package every time. On Windows that order runs MSI, Winget, Chocolatey, EXE, Scoop, MS Store, script. Determinism in package selection sounds like a detail until a policy silently picks differently on two machines that are meant to be identical.

Absent does the same work in reverse

Everything above describes keeping software on a machine. The interesting half is the other setting.

Set State to Absent and the loop is identical: evaluate, find the thing present, create an uninstall job, dispatch, run, verify it is gone. In the event stream further up, the three rows at the bottom are exactly that, the tail of a previous cycle where the same policy was set the other way.

That covers the software you are contractually obliged not to have. Not the dramatic version, usually. Usually it is a torrent client that arrived with someone’s laptop, or a remote access tool a vendor left behind after an engagement, or a licence you stopped paying for and would rather not be audited on. Those are boring problems that stay solved only if something keeps checking, because a user who wanted the software once will install it again.

What it does not do

Two honest caveats, because the model has edges.

An interval is an interval. A policy set to one hour means drift can persist for up to an hour before anything notices. That is fine for licence compliance and wrong for anything you would page someone about, and it is worth setting deliberately rather than accepting a default.

Approval is a separate gate from activation. A policy can exist, be correct, and not be approved, in which case it sits there doing nothing. We think that is the right default for something with permission to uninstall software across a tenant, but it does mean “I created the policy” and “the policy is running” are two different states and you should check which one you are in.

Neither of those is a reason to go back to the script. They are just the parts people find out later, and I would rather write them down now.

For what else runs on this same agent, the RMM overview covers monitoring, patching and scripting, and we have written separately about automation recipes that save real time.


LynxTrac is free forever for up to 2 servers, no card required. If you want to try it on real infrastructure instead of reading about it: app.lynxtrac.com.

Related posts