Skip to main content

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:

ValidatorWhat it checks
digital signature required - updaterdigitalSignature 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 - updaterinstallDir 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

PropertyTypeRequiredDefaultDescription
serviceNamestring (non-empty)yesInternal name of the Windows service. Used in sc.exe, the registry, and event logs. Should be unique system-wide.
serviceDisplayNamestringnoserviceNameThe display name shown in services.msc.
serviceDescriptionstringnoFree-form description shown in services.msc.
latestVersionDescriptorobjectyesTells the service how to fetch and parse the version feed. See below.
schedulingobjectyesPolling cadence. See below.
notificationsobjectyesUI toast policy. See below.
loggingobjectno{ "installerLogsRetentionDays": 30 }Log retention. See below.
tip

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.

PropertyTypeRequiredDescription
urlURL (HTTPS only)yesGET endpoint that returns the feed. Must be https://. The validator valid http url enforces this.
versionMatcherregexyesMatches the version string (e.g. 1.2.3). Compared against the installed ProductVersion using normal version semantics.
installerUrlMatcherregexyesMatches 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.
checkSumMatcherregexyesMatches the SHA256 hash of the MSI as a hex string. The service hashes the downloaded file and aborts on mismatch.
versionUrlMatcherregexnoMatches 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.
important

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

PropertyTypeRequiredDefaultDescription
enabledbooleanyesMaster 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.
initialDelayinteger (ms)no60000 (1 min)Delay between service start and the first check. Keeps boot quiet. Override with UPDATE_SCHEDULING_STARTUP_DELAY.
intervalinteger (ms)no86400000 (24 h)Delay between successful checks. Override with UPDATE_SCHEDULING_INTERVAL.
retryIntervalinteger (ms)no180000 (3 min)Delay after a transient failure (app running, installer busy, network blip). Override with UPDATE_SCHEDULING_RETRY_INTERVAL.

Common cadences:

Goalinterval
Daily check (default)86400000
Every 6 hours21600000
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

PropertyTypeRequiredDefaultDescription
enabledbooleanyesMaster switch for end-user notifications. Override with UPDATE_NOTIFICATIONS_ENABLED.
triggersstring[] (process names)noWhen 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:

  1. The service subscribes to Windows process-creation events (WMI). When any of the listed processes start, the service is notified instantly (no polling).
  2. 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."
  3. 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

PropertyTypeRequiredDefaultDescription
installerLogsRetentionDaysintegerno30Days 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 propertyOverridesExample
UPDATE_SCHEDULING_ENABLED=0|1scheduling.enabledUPDATE_SCHEDULING_ENABLED=0
UPDATE_SCHEDULING_STARTUP_DELAY=<ms>scheduling.initialDelayUPDATE_SCHEDULING_STARTUP_DELAY=300000
UPDATE_SCHEDULING_INTERVAL=<ms>scheduling.intervalUPDATE_SCHEDULING_INTERVAL=21600000
UPDATE_SCHEDULING_RETRY_INTERVAL=<ms>scheduling.retryIntervalUPDATE_SCHEDULING_RETRY_INTERVAL=300000
UPDATE_NOTIFICATIONS_ENABLED=0|1notifications.enabledUPDATE_NOTIFICATIONS_ENABLED=0
UPDATE_LOGGING_INSTALLER_LOGS_RETENTION_DAYS=<int>logging.installerLogsRetentionDaysUPDATE_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

SymptomLikely causeFix
Service is not installed after upgradeThe 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 mismatchThe 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 mismatchcheckSumMatcher 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 firesscheduling.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 10Server 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 updatesService 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:

ValidatorFailure triggerPointer
digital signature required - updatermsi.updater is set but digitalSignature is missing.$.msi.updater
valid installDir - updaterInstall 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 urllatestVersionDescriptor.url is not an https:// URL.$.msi.updater.latestVersionDescriptor.url
min (any non-empty string field)Empty serviceName / matchers / url.$.msi.updater.<path>