Publish to the Steam Workshop
Problem. Your mod works locally. Now it has to reach players.
Solution. steamcmd plus a .vdf manifest. The sample mods ship a working script;
this page is that script explained, including the one line you must change.
Warning
Copying a sample's Steam/ folder brings three things you must change, not one.
SteamPublish.sh contains steamcmd +login lorenzo_tobspr — a tobspr developer
account — and base.vdf still carries that sample's real publishedfileid, title and
description. Publishing with an inherited id aims your upload at someone else's
workshop item; it will be refused, but check base.vdf before your first run rather
than after. Set publishedfileid to 0, fix the title and description, and replace the
login with
your own Steam account name before running anything.
What you need
steamcmdon yourPATH(Valve's download)- A Steam account that has accepted the Workshop legal agreement — do this once in a browser, or your first upload is rejected
- bash with
envsubstandcygpathon Windows — Git Bash provides both - A
preview.png— the Workshop requires a preview image
The layout
MyMod/
├── MyMod.csproj
└── Steam/
├── base.vdf the item manifest
├── preview.png Workshop thumbnail
└── SteamPublish.sh the upload script
base.vdf
"workshopitem"
{
"appid" "2162800"
"publishedfileid" "0"
"contentfolder" "${CONTENT_PATH}"
"previewfile" "${PREVIEW_IMG}"
"visibility" "2"
"title" "My Mod"
"description" "What the mod does"
"changenote" "${CHANGE_NOTE}"
}
| Key | Meaning |
|---|---|
appid |
always 2162800 — shapez 2 |
publishedfileid |
0 creates a new item; a real id updates that item |
contentfolder |
absolute path to your built mod folder — filled in by the script |
previewfile |
absolute path to the thumbnail — filled in by the script |
visibility |
0 public, 1 friends only, 2 private, 3 unlisted |
changenote |
optional; the line shown in the item's change history |
Start with visibility "2" while testing. A half-broken public item collects
one-star ratings faster than you can fix it.
Substitute changenote rather than hard-coding it. base.vdf is a checked-in
template that every publish reuses, so a literal note would claim the same change
on every future update — export CHANGE_NOTE beside CONTENT_PATH and let
envsubst fill it, or leave the key out entirely.
The manifest carries exactly one image
previewfile is the thumbnail, and it is the only image workshop_build_item
understands. Additional screenshots and videos cannot be uploaded from the command
line at all — they are added in the item's web interface after it exists. The
Steamworks API does expose ISteamUGC::AddItemPreviewFile for extra previews, but
steamcmd does not surface it, so there is nothing to put in the manifest.
Plan for that: the first publish creates an item with one image, and the rest of the store page is filled in on the website.
How the script works
CONTENT_PATH=$1 # passed in by the csproj target
CURRENT_DIR=$(cygpath -w "$PWD") # POSIX path → Windows path
PREVIEW_IMG=$CURRENT_DIR\\Steam\\preview.png
export CONTENT_PATH
export PREVIEW_IMG
envsubst < Steam\\base.vdf > Steam\\base.tmp.vdf # substitute the two paths
steamcmd +login YOUR_ACCOUNT +workshop_build_item "$TMP_VDF" +quit
# read the id Steam assigned and write it back into base.vdf
FILE_ID=$(grep '"publishedfileid"' Steam\\base.tmp.vdf | sed 's/.*"\([0-9]\+\)".*/\1/')
sed -i 's/\("publishedfileid"[ \t]*"\)[0-9]\+"/\1'"$FILE_ID"'"/' Steam\\base.vdf
The clever part is the last step: after a successful first upload, Steam writes the new
publishedfileid into the temp manifest, and the script copies it back into
base.vdf. Every later run therefore updates the same Workshop item instead of
creating duplicates.
Commit base.vdf after your first publish. Losing that id means losing the ability
to update your own item, and there is no way to reclaim it other than publishing a new
one and asking players to re-subscribe.
Wiring it to the build
<Target Name="SteamPublish">
<Exec Command='sh .\Steam\SteamPublish.sh "$(OutputPath)"' />
</Target>
dotnet build MyMod.csproj -t:SteamPublish
$(OutputPath) is your mod folder, so the upload always ships exactly what the game
loads. Note the closing quote after $(OutputPath) — the version in the
PlatformEfficiencyOverlay sample is missing it.
What gets uploaded
Everything in contentfolder. That should be:
MyMod.dll
manifest.json
translations.json
Resources/…
It should not be game assemblies (that is what <Private>False</Private> prevents),
.pdb files unless you want them shipped, or your Steam/ folder. Look in the output
folder before your first upload — whatever is there is what the world gets.
Before you go public
manifest.jsonVersionbumped, andGameVersionSupportRangehonest about what you have testedDependencieslists ShapezShifter (steam:3542611357) with a version rangeAffectsSaveGamescorrect —trueif you write save data,falsefor a read-only overlay- Tested from a subscribed copy, not your build output. Subscribing installs to the
Workshop content folder, which is a different path from
SPZ2_PERSISTENT\modsand catches "works on my machine" path bugs
Gotchas
- First upload with
publishedfileid "0", then never again. Leaving it at0creates a fresh item every run. - The
cygpath/envsubst/double-backslash dance in the script exists becausesteamcmdwants Windows paths while the script runs in bash. On Linux or macOS you will need to simplify those lines. steamcmdmay prompt for Steam Guard on first login. Run it once interactively before wiring it into a build.- Workshop descriptions take BBCode, not Markdown.