Auto-Updater: Configuration
Applies to:
MSI only. MSIX uses Microsoft Store / App Installer flows instead – see MSIX Features.
This page covers how to configure, deploy, and troubleshoot the auto-updater. For architecture, internal flows, and security design see Auto-updater: How it works.
Prerequisites
Before MPDEV will even build a package with updater, two cross-property validators must pass:
| Validator | What it checks |
|---|---|
digital signature required - updater | digitalSignature must be configured. The updater compares the publisher signature of the old installer with the new one and refuses any upgrade if they do not match. This is the chain-of-trust that prevents a hijacked feed from pushing a different vendor's MSI. |
valid install dir - updater | installDir must resolve to a sub-directory of %ProgramFiles% or %ProgramFiles(x86)%. Per-user installs to %LocalAppData% etc. cannot host an elevated service. |
Practical implications:
- You must own a code-signing certificate (or use Azure Trusted Signing). See Digital Signature.
- You must always sign with the same certificate for the lifetime of the product. Renewing to a different cert key will break updates for already-installed users until they re-install manually.
- Per-machine installs only. Per-user updaters are not supported.
Quick start
Minimal viable updater:
{
"outputTypes": ["msi"],
"outputDirectory": "bin\\Package",
"packageName": "My Application",
"publisher": "My Company Name Ltd.",
"platform": "x64",
"version": "1.0.0",
"installDir": "%ProgramFiles%\\My Company\\Notes",
"icon": "MyApp.ico",
"fileSystemEntries": [
{ "sourcePath": "build\\MyApp.exe", "targetPath": "$.installDir\\MyApp.exe" }
],
"digitalSignature": {
"signWith": "Thumbprint",
"thumbprint": "ABCDEF0123456789...",
"timestampServer": "http://timestamp.acs.microsoft.com"
},
"msi": {
"upgradeCode": "{8E5A7B2A-2B11-4F1E-A3F1-7E0B11C4D9E0}",
"updater": {
"serviceName": "MyApplication.Updater",
"latestVersionDescriptor": {
"url": "https://updates.MyCompany.example.com/notes/latest.json",
"versionMatcher": "(?<=\"version\":\\s*\")[^\"]+",
"installerUrlMatcher": "(?<=\"url\":\\s*\")[^\"]+",
"checkSumMatcher": "(?<=\"sha256\":\\s*\")[^\"]+"
},
"scheduling": { "enabled": true },
"notifications": { "enabled": true }
}
}
}
Stand up an HTTPS endpoint that returns:
{
"version": "1.0.0",
"url": "https://downloads.MyCompany.example.com/MyApplication-1.0.0.msi",
"sha256": "AB12...EF34"
}
Build, install, and the service MyApplication.Updater will start on boot, poll the URL, and upgrade users when you bump the version field.
Top-level updater properties
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
serviceName | string (non-empty) | yes | – | Internal name of the Windows service. Used in sc.exe, the registry, and event logs. Should be unique system-wide. |
serviceDisplayName | string | no | serviceName | The display name shown in services.msc. |
serviceDescription | string | no | – | Free-form description shown in services.msc. |
latestVersionDescriptor | object | yes | – | Tells the service how to fetch and parse the version feed. See below. |
scheduling | object | yes | – | Polling cadence. See below. |
notifications | object | yes | – | UI toast policy. See below. |
logging | object | no | { "installerLogsRetentionDays": 30 } | Log retention. See below. |
Pick a serviceName that is unmistakable, e.g. MyApplication.Updater. Avoid generic names like Updater – they can collide with other vendors and confuse end users browsing services.msc.
latestVersionDescriptor – feed parser
The feed is whatever you want it to be – JSON, XML, plain text, even an HTML page. The service does a single GET and applies regex patterns against the raw response body. The full match (i.e., Regex.Match(response, pattern).Value) is what the service uses as the extracted value — so your patterns must be written so that the entire match IS the value you want. Use lookaheads/lookbehinds to anchor without including the anchor text.
| Property | Type | Required | Description |
|---|---|---|---|
url | URL (HTTPS only) | yes | GET endpoint that returns the feed. Must be https://. The validator valid http url enforces this. |
versionMatcher | regex | yes | Matches the version string (e.g. 1.2.3). Compared against the installed ProductVersion using normal version semantics. |
installerUrlMatcher | regex | yes | Matches the download URL. Must resolve to an HTTPS URL pointing at an MSI file signed with the same publisher cert. The service validates the URL scheme at runtime. |
checkSumMatcher | regex | yes | Matches the SHA256 hash of the MSI as a hex string. The service hashes the downloaded file and aborts on mismatch. |
versionUrlMatcher | regex | no | Matches a URL to release notes / "what's new". Shown to the user in the update notification. If omitted, the URL stored from the previous install is reused. |
The service uses Regex.Match(response, pattern).Value — the full match text, not a capture group. Write patterns using lookbehinds (or lookaheads) so that only the desired value is part of the match. For example, to extract 1.4.2 from "version": "1.4.2", use the pattern (?<="version":\s*")[^"]+ — the lookbehind positions the match after the key without including it.
scheduling – when to check
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | boolean | yes | – | Master switch. When false, the service still installs but never checks. Useful for managed environments where IT pushes updates centrally. Override at install with UPDATE_SCHEDULING_ENABLED. |
initialDelay | integer (ms) | no | 60000 (1 min) | Delay between service start and the first check. Keeps boot quiet. Override with UPDATE_SCHEDULING_STARTUP_DELAY. |
interval | integer (ms) | no | 86400000 (24 h) | Delay between successful checks. Override with UPDATE_SCHEDULING_INTERVAL. |
retryInterval | integer (ms) | no | 180000 (3 min) | Delay after a transient failure (app running, installer busy, network blip). Override with UPDATE_SCHEDULING_RETRY_INTERVAL. |
Common cadences:
| Goal | interval |
|---|---|
| Daily check (default) | 86400000 |
| Every 6 hours | 21600000 |
| Hourly (rapid release product) | 3600000 |
| Weekly (LOB app) | 604800000 |
Do not set the interval below ~5 minutes in production. Aggressive polling burns server bandwidth and triggers WAF rate limiters with no real upside.
notifications – prompt the user
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | boolean | yes | – | Master switch for end-user notifications. Override with UPDATE_NOTIFICATIONS_ENABLED. |
triggers | string[] (process names) | no | – | When any of these processes launch, and an update is pending, show a toast. Lets you defer the upgrade until a moment that does not interrupt the user. |
How triggers works in practice:
- The service subscribes to Windows process-creation events (WMI). When any of the listed processes start, the service is notified instantly (no polling).
- If an update is available, the service launches the Updater UI in the user's desktop session to display a toast: "A new version of [AppName] is available."
- The user can press "Update" to initiate the upgrade interactively, or dismiss the notification.
Process names are matched case-insensitively. In the INI configuration file, multiple trigger processes are separated by | (pipe character).
logging – retention
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
installerLogsRetentionDays | integer | no | 30 | Days to keep the service's scheduling, update, and installer logs. Cleanup runs on service start. Override with UPDATE_LOGGING_INSTALLER_LOGS_RETENTION_DAYS. |
Logs are written to %ProgramData%\<AppId>\Logs\ (where <AppId> is the updater's service identifier). Three types of log files are produced:
scheduler.log– timer fires, sleep durations, abort reasons.updater-<sessionId>.log– HTTPS calls, regex parse results, hash checks, signature verification for each update attempt.<installerFileName>-<sessionId>.log– the raw verbose MSI install output for each upgrade attempt.
Set retention to a higher value (e.g. 90) for slow-release LOB products where users often surface bugs weeks after the fact.
Designing the version feed
The feed format is your choice. Three patterns work well:
Pattern A – static JSON file on a CDN
The simplest possible setup: drop a JSON file next to your MSI on Azure Blob, S3, or a CDN.
https://updates.MyCompany.example.com/notes/latest.json
{
"version": "1.4.2",
"url": "https://downloads.MyCompany.example.com/MyApplication-1.4.2.msi",
"sha256": "AB12...EF34",
"releaseNotes": "https://MyCompany.example.com/notes/release-notes/1.4.2"
}
Updates are a git push to a docs repo, or an aws s3 cp. No server, no scaling.
Pattern B – channel-aware endpoint
Serve a different payload depending on the URL path so you can run stable, beta, and canary rings at the same time.
https://updates.MyCompany.example.com/notes/stable/latest.json
https://updates.MyCompany.example.com/notes/beta/latest.json
You ship two MSIs – one with the stable URL baked into latestVersionDescriptor.url, one with the beta URL.
Regex cookbook
Below are field-tested regex patterns you can paste straight into your config. Remember to escape \ as \\ in JSON strings. All patterns use lookbehinds so that the full match is just the value you want.
JSON feed
"latestVersionDescriptor": {
"url": "https://updates.MyCompany.example.com/notes/latest.json",
"versionMatcher": "(?<=\"version\":\\s*\")[^\"]+",
"versionUrlMatcher": "(?<=\"releaseNotes\":\\s*\")[^\"]+",
"installerUrlMatcher": "(?<=\"url\":\\s*\")[^\"]+",
"checkSumMatcher": "(?<=\"sha256\":\\s*\")[^\"]+"
}
Matches a response body like:
{
"version": "1.4.2",
"url": "https://downloads.MyCompany.example.com/My Application-1.4.2.msi",
"sha256": "AB12...EF34",
"releaseNotes": "https://MyCompany.example.com/notes/release-notes/1.4.2"
}
XML / RSS feed
<update>
<version>1.4.2</version>
<url>https://downloads.MyCompany.example.com/MyApplication-1.4.2.msi</url>
<sha256>AB12...EF34</sha256>
<releaseNotes>https://MyCompany.example.com/notes/release-notes/1.4.2</releaseNotes>
</update>
"latestVersionDescriptor": {
"url": "https://updates.MyCompany.example.com/notes/latest.xml",
"versionMatcher": "(?<=<version>)[^<]+",
"versionUrlMatcher": "(?<=<releaseNotes>)[^<]+",
"installerUrlMatcher": "(?<=<url>)[^<]+",
"checkSumMatcher": "(?<=<sha256>)[^<]+"
}
Plain-text key=value feed
version=1.4.2
url=https://downloads.MyCompany.example.com/MyApplication-1.4.2.msi
sha256=AB12...EF34
releaseNotes=https://MyCompany.example.com/notes/release-notes/1.4.2
"latestVersionDescriptor": {
"url": "https://updates.MyCompany.example.com/notes/latest.txt",
"versionMatcher": "(?m)(?<=^version=).+$",
"versionUrlMatcher": "(?m)(?<=^releaseNotes=).+$",
"installerUrlMatcher": "(?m)(?<=^url=).+$",
"checkSumMatcher": "(?m)(?<=^sha256=)[0-9A-Fa-f]+$"
}
GitHub Releases API
GitHub's /repos/{owner}/{repo}/releases/latest returns JSON with predictable fields:
"latestVersionDescriptor": {
"url": "https://api.github.com/repos/MyCompany/notes/releases/latest",
"versionMatcher": "(?<=\"tag_name\":\\s*\"v?)[^\"]+",
"versionUrlMatcher": "(?<=\"html_url\":\\s*\")[^\"]+",
"installerUrlMatcher": "(?<=\"browser_download_url\":\\s*\")[^\"]+\\.msi",
"checkSumMatcher": "(?<=\"sha256\":\\s*\")[0-9A-Fa-f]{64}"
}
The GitHub API does not include a SHA256 by default – you'll need to publish one in the release body or as a separate asset and adjust the regex accordingly.
Install-time overrides
End users (and IT departments) can override the cadence at install time by passing MSI properties on the msiexec command line. This is invaluable for quiet rollouts in managed environments.
| MSI property | Overrides | Example |
|---|---|---|
UPDATE_SCHEDULING_ENABLED=0|1 | scheduling.enabled | UPDATE_SCHEDULING_ENABLED=0 |
UPDATE_SCHEDULING_STARTUP_DELAY=<ms> | scheduling.initialDelay | UPDATE_SCHEDULING_STARTUP_DELAY=300000 |
UPDATE_SCHEDULING_INTERVAL=<ms> | scheduling.interval | UPDATE_SCHEDULING_INTERVAL=21600000 |
UPDATE_SCHEDULING_RETRY_INTERVAL=<ms> | scheduling.retryInterval | UPDATE_SCHEDULING_RETRY_INTERVAL=300000 |
UPDATE_NOTIFICATIONS_ENABLED=0|1 | notifications.enabled | UPDATE_NOTIFICATIONS_ENABLED=0 |
UPDATE_LOGGING_INSTALLER_LOGS_RETENTION_DAYS=<int> | logging.installerLogsRetentionDays | UPDATE_LOGGING_INSTALLER_LOGS_RETENTION_DAYS=90 |
Example "silent, no auto-update" install for a managed environment:
msiexec /i MyApplication-1.4.2.msi /qn /norestart UPDATE_SCHEDULING_ENABLED=0 UPDATE_NOTIFICATIONS_ENABLED=0
The full list of install-time properties is in CLI Reference.
Disabling the updater completely
For enterprises that manage updates through their own tooling (Configuration Manager, Intune, etc.), there are two levels of disabling:
Level 1 – Disable scheduling and notifications (service still runs)
Pass MSI properties at install time to turn off both auto-checks and user prompts:
msiexec /i MyApp-1.4.2.msi /qn /norestart UPDATE_SCHEDULING_ENABLED=0 UPDATE_NOTIFICATIONS_ENABLED=0
The service installs and starts, but enters an idle loop – it configures no timers and watches no triggers. It listens only for configuration file changes (in case a future upgrade re-enables features).
Level 2 – Remove the updater MSI feature entirely
If you do not want the service binary installed at all, add ADDLOCAL=ALL REMOVE=Updater to the installation command line. When the feature is not installed:
- No service binary is placed on disk.
- No service is registered in
services.msc. - No
%ProgramData%folders are created.
This is the recommended approach for locked-down enterprise environments that prohibit any form of self-updating software.
Operational management
Verify the service exists after install
Get-Service -Name MyApplication.Updater
Stop / disable the service for testing
Stop-Service MyApplication.Updater
Set-Service MyApplication.Updater -StartupType Disabled
Force an immediate update check
There is no public API to nudge the service. The supported way to trigger a check on demand is to restart it – initialDelay will fire shortly after.
Restart-Service MyApplication.Updater
Read the logs
$dir = "$env:ProgramData\MyApplication.Updater\Logs"
Get-ChildItem $dir
Get-Content "$dir\scheduler.log" -Tail 50
Uninstall
The updater service is uninstalled automatically when the MSI is uninstalled – it is registered as a regular MSI service component.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Service is not installed after upgrade | The upgrade was sourced from a build before updater was added. | Confirm Get-Service after a fresh install. The service appears only on installs that included msi.updater. |
updater.log shows Signature mismatch | The new MSI was signed by a different cert than the installed one. | Always sign with the same code-signing certificate. If you must rotate, push one transitional release manually first. |
updater.log shows Checksum mismatch | checkSumMatcher regex is wrong, or the feed reports a stale hash. | Hash the MSI yourself with Get-FileHash <file> -Algorithm SHA256 and confirm the feed value. Ensure your regex matches only the hash value (use a lookbehind to exclude surrounding text). |
| Updater never fires | scheduling.enabled is false, or UPDATE_SCHEDULING_ENABLED=0 was passed at install. | Inspect HKLM\Software\<publisher>\<packageName>\Updater or the install-time MSI log. |
MSI install exits with 1618 (another install in progress) | Upgrade attempted while another MSI install is running. | Service auto-retries after retryInterval. No action needed. |
| HTTPS fetch fails with TLS errors on Windows 10 | Server requires TLS 1.3 / modern ciphers not enabled by default in older OS builds. | Use TLS 1.2-friendly cipher suites on the feed CDN, or require Windows 10 22H2+ as a minimum. |
| Behind corporate proxy, no updates | Service runs as LocalSystem and inherits proxy settings from WinHTTP, not the user's IE / Edge settings. | Configure WinHTTP proxy with netsh winhttp set proxy or via Group Policy. |
Full example
A complete, signed, per-machine MSI with a daily auto-updater, weekly retention bump, and a launch trigger.
{
"outputTypes": ["msi"],
"outputDirectory": "bin\\Package",
"packageName": "My Application",
"publisher": "My Company Name Ltd.",
"platform": "x64",
"version": "1.0.0",
"installDir": "%ProgramFiles%\\My Company\\Notes",
"icon": "MyApp.ico",
"fileSystemEntries": [
{ "sourcePath": "build\\MyApp.exe", "targetPath": "$.installDir\\MyApp.exe" },
{ "sourcePath": "build\\My Application.Service.exe", "targetPath": "$.installDir\\My Application.Service.exe" }
],
"shortcuts": [
{
"name": "My Application",
"target": "$.installDir\\MyApp.exe"
}
],
"digitalSignature": {
"signWith": "AzureTrustedSigning",
"endpoint": "https://eus.codesigning.azure.net/",
"codeSigningAccountName": "MyCompanySigning",
"certificateProfileName": "MyCompanyReleaseProfile",
"timestampServer": "http://timestamp.acs.microsoft.com"
},
"msi": {
"upgradeCode": "{8E5A7B2A-2B11-4F1E-A3F1-7E0B11C4D9E0}",
"updater": {
"serviceName": "MyApplication.Updater",
"serviceDisplayName": "My Application Updater",
"serviceDescription": "Checks for and silently installs My Application updates.",
"latestVersionDescriptor": {
"url": "https://updates.MyCompany.example.com/notes/latest.json",
"versionMatcher": "(?<=\"version\":\\s*\")[^\"]+",
"versionUrlMatcher": "(?<=\"releaseNotes\":\\s*\")[^\"]+",
"installerUrlMatcher": "(?<=\"url\":\\s*\")[^\"]+",
"checkSumMatcher": "(?<=\"sha256\":\\s*\")[0-9A-Fa-f]{64}"
},
"scheduling": {
"enabled": true,
"initialDelay": 60000,
"interval": 86400000,
"retryInterval": 180000
},
"notifications": {
"enabled": true,
"triggers": ["MyApp.exe"]
},
"logging": {
"installerLogsRetentionDays": 90
}
}
}
}
Validation summary
mpdev build rejects the package with a clear error if any of the following fail:
| Validator | Failure trigger | Pointer |
|---|---|---|
digital signature required - updater | msi.updater is set but digitalSignature is missing. | $.msi.updater |
valid installDir - updater | Install directory ($.installDir) must be set as sub-directory of the %ProgramFiles% or %ProgramFiles(x86)% directory to enable updater for MSI packages. | $.msi.updater |
required (serviceName, latestVersionDescriptor, scheduling, notifications) | Any of the four required children of updater is missing. | $.msi.updater.<child> |
valid http url | latestVersionDescriptor.url is not an https:// URL. | $.msi.updater.latestVersionDescriptor.url |
min (any non-empty string field) | Empty serviceName / matchers / url. | $.msi.updater.<path> |