Update a 7 Days to Die Dedicated Server Only When There Is a New Build
Compare the buildid in the appmanifest with SteamCMD’s app_info_print and run app_update only when they differ. Success! and exit code 0 prove nothing.

On this page
TL;DR
Read the installed build number from steamapps\appmanifest_294420.acf, read the published one from SteamCMD’s app_info_print, and run app_update only when they differ.
Check the result by reading the installed build again, because SteamCMD can print Success! and exit with code 0 without changing the build. It did on a server installed from a branch, until -beta public was added to the command.
In PowerShell on the server, this reads both numbers. It prints the build you have, and the one Steam publishes on the public branch:
$manifest = 'C:\7DaysToDieServer\steamapps\appmanifest_294420.acf'
$installed = (Select-String -LiteralPath $manifest -Pattern '^\t"buildid"\s+"(\d+)"' |
Select-Object -First 1).Matches[0].Groups[1].Value
$arguments = '+login', 'anonymous', '+app_info_update', '1',
'+app_info_print', '294420', '+quit'
$info = (& 'C:\steamcmd\steamcmd.exe' $arguments 2>&1) -join "`n"
$pattern = '(?s)"branches"\s*\{.*?"public"\s*\{[^}]*?"buildid"\s+"(\d+)"'
$published = [regex]::Match($info, $pattern).Groups[1].Value
"installed $installed, published $published"The game-specific parts are the app ID, 294420, and the file that holds the build number. Step 4 has the whole update as one function.
The previous parts installed the server, configured it, and made it start at boot and stop cleanly. This part keeps it current without restarting it for nothing. A server that runs app_update every morning restarts on the days when Steam has published nothing, and its players lose the session for no reason.
1. Read the installed build from appmanifest_294420.acf
SteamCMD records what it installed in steamapps\appmanifest_294420.acf, inside the server’s folder. The file is Valve’s KeyValues text, and the installed build is the top-level buildid line:
PS> $manifest = 'C:\7DaysToDieServer\steamapps\appmanifest_294420.acf'
PS> Select-String -Path $manifest -Pattern 'buildid' | ForEach-Object LineOutput
"buildid" "24994542"
"TargetBuildID" "24994542"TargetBuildID is a different key. The pattern in the next function has quotes around buildid, so it doesn’t match that line. It also requires one leading tab, which makes it the top-level key and not one nested deeper. The first function reads the number and returns nothing when the file or the line is missing. The second reads the branch the install came from, which the manifest records as BetaKey, and gives public when there is none:
function Get-InstalledBuild {
param([string]$ServerDir = 'C:\7DaysToDieServer', [string]$AppId = '294420')
$manifest = Join-Path $ServerDir "steamapps\appmanifest_$AppId.acf"
if (-not (Test-Path -LiteralPath $manifest)) { return }
$match = Select-String -LiteralPath $manifest -Pattern '^\t"buildid"\s+"(\d+)"' |
Select-Object -First 1
if ($match) { $match.Matches[0].Groups[1].Value }
}
function Get-InstalledBranch {
param([string]$ServerDir = 'C:\7DaysToDieServer', [string]$AppId = '294420')
$manifest = Join-Path $ServerDir "steamapps\appmanifest_$AppId.acf"
if (-not (Test-Path -LiteralPath $manifest)) { return 'public' }
$match = Select-String -LiteralPath $manifest -Pattern '"BetaKey"\s+"([^"]+)"' |
Select-Object -First 1
if ($match) { $match.Matches[0].Groups[1].Value } else { 'public' }
}2. Read the published build from app_info_print
app_info_print prints everything Steam has on the app, about 560 lines for this server. The part that matters is branches, which has one entry per branch, each with its own buildid:
"branches"
{
"public"
{
"buildid" "24994542"
"timeupdated" "1788196235"
"timebuildupdated" "1787927331"
}
"v3.2.0"
{
"buildid" "24994542"
"description" "Version 3.2.0 Stable"
"timeupdated" "1788195715"
"timebuildupdated" "1787927331"
}
"latest_experimental"
{
"buildid" "25661908"
"description" "Unstable build"
"timeupdated" "1790898315"+app_info_update 1 makes SteamCMD fetch fresh data before it prints, and the whole lookup took about three seconds here. Take the buildid of the branch you run, which is public unless you chose a beta. The same text holds a public entry under each depot, with a gid and no buildid. A search from the first public therefore depends on where the sections happen to sit. The pattern here starts at branches:
function Get-PublishedBuild {
param(
[string]$SteamCmd = 'C:\steamcmd\steamcmd.exe',
[string]$AppId = '294420',
[string]$Branch = 'public'
)
if (-not (Test-Path -LiteralPath $SteamCmd)) { return }
$arguments = '+login', 'anonymous', '+app_info_update', '1',
'+app_info_print', $AppId, '+quit'
$text = (& $SteamCmd $arguments 2>&1) -join "`n"
$pattern = '(?s)"branches"\s*\{.*?"' + [regex]::Escape($Branch) +
'"\s*\{[^}]*?"buildid"\s+"(\d+)"'
$match = [regex]::Match($text, $pattern)
if ($match.Success) { $match.Groups[1].Value }
}It returns nothing when SteamCMD isn’t where you said, or when the text has no such branch, which is what a failed connection to Steam gives too. A branch that doesn’t exist, nonexistent here, gave an empty result. Branch names are case-sensitive, and a branch that needs a password isn’t covered.
3. Compare the two builds, and treat a missing number as unknown
Run both functions:
PS> Get-InstalledBuildOutput
24994542PS> Get-PublishedBuildOutput
24994542Equal numbers mean there is nothing to do. When either is empty, you don’t know, and the safe answer is to stop and say so, not to guess. The next function does that.
4. Update only when they differ
This function does the whole job. When the builds match and the install is already on the branch you want, it returns at once and touches nothing, so a running server keeps running. When they differ it stops the server if one is running, runs app_update, reads the installed build again to check, and starts the server if it was running:
function Update-GameServer {
param(
[string]$ServerDir = 'C:\7DaysToDieServer',
[string]$SteamCmd = 'C:\steamcmd\steamcmd.exe',
[string]$AppId = '294420',
[string]$Branch,
[scriptblock]$StopServer = { Stop-GameServer },
[scriptblock]$StartServer = {
Start-ScheduledTask -TaskName '7DaysToDieServer' -ErrorAction Stop
}
)
if (Test-Path -LiteralPath $ServerDir) {
$ServerDir = (Resolve-Path -LiteralPath $ServerDir).ProviderPath
}
if (-not $Branch) {
$Branch = Get-InstalledBranch -ServerDir $ServerDir -AppId $AppId
}
$installed = Get-InstalledBuild -ServerDir $ServerDir -AppId $AppId
$published = Get-PublishedBuild -SteamCmd $SteamCmd -AppId $AppId -Branch $Branch
"Installed build: $(if ($installed) { $installed } else { 'unknown' })"
"Published build: $(if ($published) { $published } else { 'unknown' })"
if (-not $installed -or -not $published) {
throw 'Could not read both builds. Nothing was changed.'
}
$installedBranch = Get-InstalledBranch -ServerDir $ServerDir -AppId $AppId
$sameBuild = $installed -eq $published
if ($sameBuild -and $installedBranch -eq $Branch) {
return 'Already on the published build. Nothing to do.'
}
$exe = Join-Path $ServerDir '7DaysToDieServer.exe'
$wasRunning = [bool](Get-Process 7DaysToDieServer -ErrorAction SilentlyContinue |
Where-Object { $_.Path -eq $exe })
$restart = $wasRunning
if ($restart) { & $StopServer }
$arguments = '+force_install_dir', $ServerDir, '+login', 'anonymous',
'+app_update', $AppId, '-beta', $Branch, 'validate', '+quit'
foreach ($attempt in 1, 2) {
$null = & $SteamCmd $arguments 2>&1
$after = Get-InstalledBuild -ServerDir $ServerDir -AppId $AppId
$now = Get-InstalledBranch -ServerDir $ServerDir -AppId $AppId
$onBranch = $now -eq $Branch
if ($after -eq $published -and $onBranch) { break }
# Steam may have published another build since the first lookup.
$published = Get-PublishedBuild -SteamCmd $SteamCmd -AppId $AppId `
-Branch $Branch
if ($after -eq $published -and $onBranch) { break }
}
$note = if ($restart) { ' The server was left stopped.' }
if (-not $after -or -not $published -or $after -ne $published) {
$shown = if ($after) { $after } else { 'unknown' }
$wanted = if ($published) { $published } else { 'unknown' }
throw "Update failed: installed build is $shown, expected $wanted.$note"
}
$afterBranch = Get-InstalledBranch -ServerDir $ServerDir -AppId $AppId
if ($afterBranch -ne $Branch) {
throw "Update failed: the install is on branch $afterBranch, not $Branch.$note"
}
if ($restart) {
& $StartServer
$deadline = (Get-Date).AddSeconds(60)
do {
$running = Get-Process 7DaysToDieServer -ErrorAction SilentlyContinue
$started = [bool]($running | Where-Object { $_.Path -eq $exe })
if (-not $started) { Start-Sleep -Seconds 2 }
} until ($started -or (Get-Date) -gt $deadline)
if (-not $started) {
throw "Updated to build $after, but the server did not start."
}
}
if ($sameBuild) { return "Switched to branch $Branch. The build is $after." }
"Updated to build $after."
}Run it as Update-GameServer. Stop-GameServer is the function from the previous part, so define it first, and the default start command is that part’s scheduled task. Change either with -StopServer and -StartServer. With a telnet password, read it once and pass it through -StopServer: $telnet = Read-Host 'Telnet password' -AsSecureString, then Update-GameServer -StopServer { Stop-GameServer -Password $telnet }.
What it does around the update:
- It resolves
-ServerDirto a full path first, so a relative path such as.\old-serverfinds the running server too. - Without
-Branchit uses the branch the manifest records, so a server on a beta stays on it. Passing-Branchmoves a server to another one. - Two branches can have the same build. Then the function runs
app_updateand saysSwitched to branch .... It still stops and starts a running server, because a build published in the meantime would change the files under it. - It runs
app_updatea second time if the first leaves the wrong build or the wrong branch. Moving frompublicto an older branch printedSuccess!and changed nothing until the second run. - A failed update stops the function with an error and leaves the server stopped on purpose, because a half-updated install that starts and reports success is worse.
- If the build doesn’t match, it asks Steam for the published build once more before it gives up, because Steam can publish a newer build between the first lookup and the update.
- After the update it waits up to a minute for the server process, since
Start-ScheduledTaskonly submits the request, and stops with an error if none appears. - A build number it can’t read, before or after the update, counts as a failed update too, and the message says
unknownfor it.
On a server that is current, with the server running:
PS> Update-GameServerOutput
Installed build: 24994542
Published build: 24994542
Already on the published build. Nothing to do.The server’s process ID didn’t change, and the manifest wasn’t touched.
Real update from branch v3.1.0 to public
To test an update that has something to download, I installed the previous stable version into a separate folder with -beta v3.1.0. That is build 24436799. I started its server and ran the function against it:
PS> Update-GameServer -ServerDir C:\scratch\old-server -Branch public `
> -StartServer { <start command for that folder> }Output
Installed build: 24436799
Published build: 24994542
Stopped, exit code 0.
Updated to build 24994542.The old server was stopped over telnet and exited with code 0. The update downloaded about 8.4 GB in a little over two minutes, and the server started again on the new build. The stop line comes from the previous part’s function.
Plain app_update exits 0 and the build doesn’t change
The command I started with was the plain app_update 294420 validate, which is what an install script usually has. On the v3.1.0 install it printed Success! App '294420' fully installed., exited with code 0, and left the build at 24436799. On this install SteamCMD had recorded the branch in the manifest as BetaKey, and a plain app_update stayed on it. The manifest read v3.1.0 before, and public after -beta public.
Running -beta public on a server that was already on public changed nothing: the build stayed current.
Staying on its own branch is right for a server that should stay on one, which is why -Branch defaults to the manifest’s BetaKey. It also means the exit code and the Success! line say nothing about whether the build you wanted landed. The function therefore reads the installed build again and compares it with the published one.
validate restores the shipped config
validate checks every file against Steam’s copy and restores any shipped file you edited. It put serverconfig.xml back to the shipped version when part 2 ran it, and again here. If you run the server from myserver.xml, as part 3 does, an update leaves your settings alone.
Common failures
Update failed: installed build is not the published one
Error
Update failed: installed build is 24436799, expected 24994542. The server was left stopped.The function ran app_update and the installed build didn’t become the published one. I couldn’t make a real update fail, so I got this message by wrapping SteamCMD in a script that answers app_update with an error. Read what SteamCMD prints by running its command yourself, with the arguments from the function. If the server was running, it stays stopped. Start it again once you have the installed build right, or after you decide to run on the old one.
Update reports success and the build is the old one
The manifest’s BetaKey still names the branch the install came from, so a plain app_update stays on it. Pass -beta with the branch you want, which the function does through -Branch, and read the installed build again.
Could not read both builds
The function stopped before changing anything, and a mistyped -Branch ends up here too. The installed build is unknown when the manifest file is missing, which happens when -ServerDir is wrong. The published build is unknown when SteamCMD isn’t where you said, can’t reach Steam, or has no such branch. Check with Get-PublishedBuild -Branch <name> and read the printed text.
Update landed and the server didn’t start
The update itself finished, and the function says so: Updated to build <build>, but the server did not start. Look at the task’s last result with Get-ScheduledTaskInfo and at the server’s log.
Update-GameServer starts the server only if one was running when it began. A server that was stopped stays stopped, and gives no such error.
What I didn’t test
I didn’t see Steam publish a new build on public while this was running, so the update tested here was a move from an older branch to the current public build. I didn’t test a branch that needs a password, or a path to SteamCMD that has spaces in it. It exercises the same download, stop and start steps, but a routine patch is smaller. The article doesn’t schedule the check. And the stop and start commands are the previous parts’, which I ran separately.
Tested with
- OS
- Windows Server 2025 Datacenter (10.0.26100), virtual machine
- PowerShell
- Windows PowerShell 5.1, elevated
- Game
- 7 Days to Die dedicated server (294420), V 3.2.0 (b10)
- Date
- October 2026