Before you start
Your game's App ID is shown in the Steamworks App Admin panel. Every app also gets at least one Depot ID, usually App ID + 1. Depots are the containers your game files are uploaded into. Find them under SteamPipe → Depots.
Get the latest SDK zip from partner.steamgames.com under Downloads. Extract it anywhere. Everything you need lives in sdk/tools/ContentBuilder/.
ContentBuilder/ ├── builder/ steamcmd for Windows ├── builder_osx/ steamcmd for macOS ├── builder_linux/ steamcmd for Linux ├── content/ put your game files here ├── output/ build logs and cache (auto-generated) └── scripts/ your .vdf build scripts go here
Copy your final game build into a subfolder of content/, for example content/windows/. Only ship what players need. Exclude debug symbols and editor leftovers (see engine notes at the bottom).
Write the build scripts
Two small text files in scripts/ describe what to upload. Replace 1000 and 1001 with your real App ID and Depot ID.
scripts/app_build_1000.vdf
"AppBuild"
{
"AppID" "1000"
"Desc" "v1.0.0 launch build"
"ContentRoot" "..\content\"
"BuildOutput" "..\output\"
"Depots"
{
"1001" "depot_build_1001.vdf"
}
}
scripts/depot_build_1001.vdf
"DepotBuild"
{
"DepotID" "1001"
"ContentRoot" "..\content\windows\"
"FileMapping"
{
"LocalPath" "*"
"DepotPath" "."
"Recursive" "1"
}
"FileExclusion" "*.pdb"
}
The Desc text is what you'll see in the Steamworks build list, so make it meaningful, like a version number. FileExclusion lines can be repeated to skip more patterns. On macOS and Linux you can use forward slashes in paths.
Shipping for several operating systems? Create one depot per OS (e.g. 1001 Windows, 1002 macOS, 1003 Linux), one depot script each, and list them all in the app script. Assign each depot to its OS in Steamworks → Installation → General Installation.
Run the upload
cd C:\path\to\sdk\tools\ContentBuilder
builder\steamcmd.exe +login your_steam_account +run_app_build ..\scripts\app_build_1000.vdf +quit
steamcmd updates itself on first run, then asks for your password and a Steam Guard code. After the first successful login, credentials are cached and future uploads won't prompt.
Upload speed depends on your connection and build size. Only changed files are uploaded on subsequent builds, so updates are much faster than the first push.
cd /path/to/sdk/tools/ContentBuilder
chmod +x builder_osx/steamcmd.sh ./builder_osx/steamcmd.sh +login your_steam_account +run_app_build ../scripts/app_build_1000.vdf +quit
If macOS Gatekeeper blocks steamcmd, allow it under System Settings → Privacy & Security, or remove the quarantine flag:
xattr -dr com.apple.quarantine builder_osx
First run only. Look for "Successfully finished AppID 1000 build" at the end. Reminder for Mac builds: ship a proper .app bundle, and codesign + notarize it or players will face Gatekeeper warnings.
# Debian / Ubuntu sudo apt update && sudo apt install lib32gcc-s1 # Arch sudo pacman -S lib32-gcc-libs
cd /path/to/sdk/tools/ContentBuilder chmod +x builder_linux/steamcmd.sh ./builder_linux/steamcmd.sh +login your_steam_account +run_app_build ../scripts/app_build_1000.vdf +quit
Enter your password and Steam Guard code on first run.
You want "Successfully finished AppID 1000 build". Linux tip: make sure your game binary has the executable bit set inside the depot, and set the correct executable path in your Steamworks launch options.
Set the build live
Your new upload appears at the top of the build list with the Desc text from your script.
Pick the build, choose a branch in the dropdown, then Preview Change → Set Build Live. The default branch is what players get. For testing, create a beta branch (optionally password-protected) under SteamPipe → Builds → Manage Branches and put the build there first.
Under Installation → General Installation, add a launch option per OS pointing at your executable. Without this, Steam doesn't know what to run. Then publish the change via the Publish tab.
Install the game through the Steam client on a clean machine (or at least a clean folder), ideally via your beta branch. This catches missing redistributables like the VC++ runtime, the number one day-one bug.
Easier ways to upload
Inside the SDK at sdk/tools/SteamPipeGUI.zip is an official Windows app that wraps everything above in a form. Unzip it, run it, fill in your App ID, Depot ID, content folder and Steamworks login, and click upload. It generates the .vdf scripts and runs steamcmd for you, and shows the log live. If the command line above made you nervous, start here; you can switch to scripts later for automation.
Steamworks can ingest a build directly in the browser: go to your app → SteamPipe → Builds and use the upload option to submit a zip of your game folder. Steamworks unpacks it into your depot. This is the simplest path for small games and first uploads. Limits: it suits smaller build sizes, gives you less control over depot mappings and exclusions, and is slower for frequent updates since the whole zip uploads every time, whereas steamcmd only uploads changed files.
First ever upload or tiny game: browser. Regular updates on Windows: SteamPipeGUI. Multi-OS depots, big builds or CI automation: the steamcmd scripts above. They all produce identical builds in the same build list.
Assigning builds: branches, depots, packages
Three concepts trip up every newcomer. Once they click, build management is easy.
A depot is a container of files (one per OS is the usual split). A package is what a customer actually buys or activates, and it lists which depots they receive. Check App Admin → All Associated Packages and make sure every depot you upload is included in your store package, or buyers will download nothing. New depots are NOT added to packages automatically.
Under SteamPipe → Builds every upload appears in a list. A build does nothing until assigned to a branch. The default branch is what all players get. Create extra branches under Manage Branches: a password-protected beta branch is the standard pre-release testing setup. Players opt in via the game's Properties → Betas in their Steam client and enter the password.
Select the build, pick the branch, Preview Change, then Set Build Live. Two situations: if your game is unreleased, putting a build on default is safe; nobody can download until you press the actual Release button on launch day, and that release flow confirms which build ships. If your game is released, setting a build live on default pushes the update to every player within minutes, so test on a beta branch first. Default-branch changes for released games ask for re-confirmation in Steamworks (and may require your login password) precisely because of that.
Shipped a broken update? Set the previous build live on default again; the build list keeps your history. This takes one minute and is the reason you never delete old builds.
Engine notes: what to exclude
| Engine | Exclude from your depot |
|---|---|
| Unity | The *_BurstDebugInformation_DoNotShip folder, *.pdb files, and any *_BackUpThisFolder_ButDontShipItWithYourGame folder. |
| Unreal | *.pdb debug symbols and the Saved/Intermediate folders. Package a Shipping build, not Development. |
| Godot | Export release templates, not debug. Exclude .import caches if exporting manually. |
Add these as extra "FileExclusion" lines in your depot script so they're stripped automatically on every upload.
Common errors
| Error | Fix |
|---|---|
| Failed to commit build | Usually wrong App/Depot ID in the scripts, or your account lacks the "Edit App Metadata" and "Publish" permissions for this app. |
| Logged in elsewhere | steamcmd logins can collide with a logged-in Steam client on the same account. Use a dedicated builder account with limited permissions. |
| Rate Limit Exceeded | Too many failed login attempts. Wait 30 minutes, double-check the password. |
| content root not found | The relative paths in the .vdf resolve from the scripts folder. Check ContentRoot points at a folder that exists. |
| Depot ... is not included | The depot isn't attached to a package. Check the depot is listed in your app's package under App Admin → All Associated Packages. |