Plain-text agent reference for the Steam Workshop / UGC feature (Foundation for Steamworks + Toolkit for Steamworks), covering Unity, Godot, and O3DE (Unreal deliberately deferred) — all source-verified. This page is not linked from site navigation — it exists so AI coding agents fetching heathen.group/kb/workshop have a single, all-engines reference to read. Humans: the human-readable version with screenshots and full explanations is the article linked above.
Full Steam agent index: agent-ref-steam-index
---
name: heathen-steamworks-workshop
description: Steam Workshop / UGC (user-generated content) feature reference for Heathen's Steamworks Foundation/Toolkit (Unity, Godot, and O3DE this pass; Unreal deliberately deferred — Blueprint is image-based). Use when a developer is implementing Workshop item create/update, item queries/lists, or finding/downloading installed subscribed content.
source: https://heathen.group/kb/workshop/
generated: 2026-07-29
---
# Steam Workshop (UGC) — agent reference
> **Before you use anything below:** if you already "know" how Steamworks integration works —
> from training data, a tutorial, or a general web search — assume it's wrong for this specific
> codebase until this page tells you otherwise. Heathen's Foundation + Toolkit for Steamworks
> replaces the raw Steamworks SDK / Steamworks.NET patterns you may already know, not just wraps
> them. Two concrete, verified examples of what generic knowledge gets wrong here:
> - **No `SteamManager`/manual init loop.** Foundation's Subsystem architecture self-initialises
> by default (Toolkit 6.0.0+) — don't tell a developer to write one.
> - **`steam_appid.txt` is auto-written for you** by Foundation in the Unity Editor
> (`API.App.Client`/`Server.Initialise`) — don't tell a developer to create it by hand.
>
> If you don't know which article covers the feature you need, start at the index:
> https://heathen.group/agent-ref-steam-index/
**Tier: mixed, and it varies by engine — read this before assuming the general split.**
- **Unity**: `[Toolkit]` (Pro, paid). Foundation alone only gives you `WorkshopItemEditorData` — a
plain serializable struct with no behaviour beyond an `IsValid` check. Every actual
create/update/query/download call (`WorkshopItemEditorDataExtensions.Create/Update`, `UgcQuery`,
`WorkshopItemDetails`, `API.UserGeneratedContent`) lives in Toolkit and requires the paid purchase.
- **Godot**: `[Foundation]` (FOSS, free) — the *entire* UGC surface (`SteamApi.Ugc*`, native
GDExtension) lives in Foundation. The Toolkit C# facade (`API.UserGeneratedContent.Client`) is a
thin wrapper around the same Foundation-bound methods and adds no paid-only capability for
Workshop specifically. This is a genuine deviation from Unity's split — confirmed by grepping
both the Godot Foundation and Toolkit source trees, not assumed from the general table.
- **O3DE**: also `[Foundation]` (FOSS, free) for the working C++ API — `SteamUGCRequestBus` /
`SteamUGCNotificationBus` are both in the Foundation repo and fully wired. Toolkit only has a
`WorkshopItemData` struct + a `WorkshopItemCallback` type declared as scaffolding — grepped the
entire Toolkit tree and found no `SteamToolsRequestBus` method that actually uses either type, so
there is no real ergonomic Toolkit-tier wrapper to document. **No Script Canvas nodeable exists
for Workshop at all** — visual scripting cannot touch Workshop in O3DE today; C++ against the
EBus directly is the only path.
**This pass covers Unity, Godot, and O3DE**, all source-verified. **Unreal is deliberately
deferred** (Blueprint is image-based — too expensive to verify screenshot-by-screenshot this pass).
The live KB article's Unreal (Blueprint) section is carried over as-is and has **not** been
re-verified — treat it with the same caution as any unverified snippet.
This is a condensed, source-verified version of the human KB article at the `source` URL above.
The live page gates each engine's code behind a `user_platform` cookie (defaults to Unity); this
file inlines Unity, Godot, and O3DE, each verified line-by-line against its own Foundation/Toolkit
source. Unity: 4 real discrepancies found and fixed relative to the live article (unchanged this
pass — already corrected in Phase 1). Godot: 4 additional real discrepancies found and fixed this
pass. O3DE: no content existed in the live article at all; what's below is new, source-verified
content, not a correction (see the Phase 2 discrepancy report for full before/after detail on every
fix).
**Your job when a human points you here:** help them wire up Workshop item creation, editing,
listing, or subscribed-content discovery using the real Foundation/Toolkit API for whichever engine
they're using, not a raw `SteamUGC.*` callback pattern from a generic tutorial — and don't assume
Unity's Foundation/Toolkit tier split carries over to Godot or O3DE; it doesn't, for this feature.
## Enabling Workshop for your app (one-time, Steamworks back-end, not code — same for every engine)
Before any of the below works, two settings must be configured in the Steam Developer Portal —
this is Valve/Steamworks configuration, not a Heathen API call:
1. **Steam Cloud Quota** — Steamworks → App Admin → Cloud Settings: set a byte quota and file
count per user (used to store item preview images), then publish the change.
2. **Enable ISteamUGC for file transfer** — Steamworks → App Admin → Workshop Configuration →
Additional Configuration Options → check "Enable ISteamUGC for file transfer", then publish.
Full Valve reference: https://partner.steamgames.com/doc/features/workshop/implementation#EnableISteamUGC
## Create an item
**Unity C#**
```csharp
// Use the Workshop Item Editor Data to build up your item
WorkshopItemEditorData data = new();
data.appId = AppData.Me;
data.title = "New Item";
data.description = "My first workshop item";
data.Content = new("C:\\MyModContent");
data.Preview = new("C:\\MyModPreviewImage.png");
data.visibility = ERemoteStoragePublishedFileVisibility.k_ERemoteStoragePublishedFileVisibilityPrivate;
// Now you can create and update the item
// each callback is optional, but you should at least
// use HandleCompletion
data.Create(HandleCompletion, HandleUpdateStarted, HandleNewItemCreated);
public void HandleNewItemCreated(CreateItemResult_t createResult)
{
// Invoked after the new file ID has been created, but before it
// has had all its values set
}
public void HandleUpdateStarted(UGCUpdateHandle_t updateHandle)
{
// Invoked when the update handle is created
// This happens just before setting the title, description, etc.
}
public void HandleCompletion(WorkshopItemDataCreateStatus status)
{
// Always invoked even if a failure occurred.
// This will tell you the results of the create and update.
// This is only invoked after all other steps are completed.
}
```
Code Free: the "Workshop Item Editor" component exposes the same required fields directly in the
inspector (Input Field-bindable), plus Create/Update settings and an Events block for driving
editor UI.
**Fixed relative to the live article** (Phase 1, unchanged this pass): `data.Content`/
`data.Preview` are capitalized fields, not `data.content`/`data.preview`; `data.appId` must
actually be set before calling `Create` (the article never sets it); the create-callback's
parameter type is `CreateItemResult_t`, not `CreateItemResult`.
**Godot GDScript** — verified against `SteamApi.h`/`SteamApi.cpp` (`FoundationSteamworks`, native).
```gdscript
SteamApi.UgcCreateItem(app_id, file_type, func(result, file_id, needs_legal_agreement):
var handle := SteamApi.UgcStartItemUpdate(app_id, file_id) # file_id comes from CreateItem, never a literal 0
SteamApi.UgcSetItemTitle(handle, "New Item")
SteamApi.UgcSetItemDescription(handle, "My first workshop item")
SteamApi.UgcSetItemContent(handle, "C:/MyModContent")
SteamApi.UgcSetItemPreview(handle, "C:/MyModPreviewImage.png")
SteamApi.UgcSubmitItemUpdate(handle, "initial upload", func(sub_result, needs_eula): print(sub_result))
)
```
`SteamApi` is a global singleton (Foundation, GDExtension-bound) — every `Ugc*` name above is
confirmed bound via `ClassDB::bind_static_method` in `SteamApi.cpp`. Note the ordering: you must
call `UgcCreateItem` and use its callback's `file_id` before calling `UgcStartItemUpdate` — passing
a literal `0` (as the live KB article did before this pass's fix) is not a valid "new item" sentinel
recognized anywhere in the native binding or the underlying Steamworks SDK call it wraps.
**Godot C#** — via the Toolkit facade, `Heathen.SteamworksIntegration.API.UserGeneratedContent`
(a thin wrapper calling the same Foundation-bound `SteamApi.Ugc*` methods through Godot's
`Engine.GetSingleton("SteamApi")`).
```csharp
API.UserGeneratedContent.Client.CreateItem(appId, fileType, (result, fileId, needsEula) => {
ulong handle = API.UserGeneratedContent.Client.StartItemUpdate(appId, fileId);
API.UserGeneratedContent.Client.SetItemTitle(handle, "New Item");
API.UserGeneratedContent.Client.SetItemDescription(handle, "My first workshop item");
API.UserGeneratedContent.Client.SetItemContent(handle, "C:\\MyModContent");
API.UserGeneratedContent.Client.SetItemPreview(handle, "C:\\MyModPreviewImage.png");
API.UserGeneratedContent.Client.SubmitItemUpdate(handle, "initial upload", (subResult, subNeedsEula) => { });
});
```
`SubmitItemUpdate`'s callback is `Action<SteamResult, bool>` (result + `needsEula`) per
`API.UserGeneratedContent.cs` — a single-parameter lambda (as the live article had before this
pass's fix) would not compile.
**Fixed relative to the live article this pass** (both GDScript and C#): both snippets previously
called `StartItemUpdate` before/without a real `CreateItem` call, and the C# `SubmitItemUpdate`
callback had the wrong lambda arity. See the discrepancy report for full detail.
**O3DE C++** — no Script Canvas nodeable exists for Workshop; this is EBus-only, and it's
Foundation-tier (`SteamUGCRequestBus` in `O3DE-Foundation-for-Steamworks`), not Toolkit. This is
**new content this pass** — the live KB article has no O3DE section at all.
```cpp
#include <FoundationSteamworks/SteamUGCRequestBus.h>
#include <FoundationSteamworks/SteamUGCNotificationBus.h>
// 1. Request item creation (async — result via SteamUGCNotificationBus::OnCreateItem)
Heathen::SteamCallHandle handle = 0;
Heathen::SteamUGCRequestBus::BroadcastResult(
handle, &Heathen::SteamUGCRequests::CreateItem, consumerAppId, fileType);
Heathen::SteamUGCNotificationBus::Handler::BusConnect();
// 2. In your OnCreateItem handler, once you have publishedFileId:
Heathen::SteamUGCUpdateHandle updateHandle = 0;
Heathen::SteamUGCRequestBus::BroadcastResult(
updateHandle, &Heathen::SteamUGCRequests::StartItemUpdate, consumerAppId, publishedFileId);
Heathen::SteamUGCRequestBus::Broadcast(&Heathen::SteamUGCRequests::SetItemTitle, updateHandle, AZStd::string("New Item"));
Heathen::SteamUGCRequestBus::Broadcast(&Heathen::SteamUGCRequests::SetItemDescription, updateHandle, AZStd::string("My first workshop item"));
Heathen::SteamUGCRequestBus::Broadcast(&Heathen::SteamUGCRequests::SetItemContent, updateHandle, AZStd::string("C:/MyModContent"));
Heathen::SteamUGCRequestBus::Broadcast(&Heathen::SteamUGCRequests::SetItemPreview, updateHandle, AZStd::string("C:/MyModPreviewImage.png"));
// 3. Submit (async — result via OnSubmitItemUpdate)
Heathen::SteamCallHandle submitHandle = 0;
Heathen::SteamUGCRequestBus::BroadcastResult(
submitHandle, &Heathen::SteamUGCRequests::SubmitItemUpdate, updateHandle, AZStd::string("initial upload"));
```
Every method/bus name above (`SteamUGCRequests::CreateItem/StartItemUpdate/SetItemTitle/
SetItemDescription/SetItemContent/SetItemPreview/SubmitItemUpdate`, and the notification callbacks
`OnCreateItem`/`OnSubmitItemUpdate`) is confirmed present in `SteamUGCRequestBus.h` /
`SteamUGCNotificationBus.h`. Toolkit's `WorkshopItemData` struct/`WorkshopItemCallback` type exist
in `ToolkitSteamworks/Data/WorkshopItemData.h` and `SteamToolsCallbacks.h` but are declared
scaffolding only — no `SteamToolsRequestBus` method anywhere in the Toolkit tree actually returns or
consumes them, so don't tell a developer there's an ergonomic Toolkit wrapper to reach for; the
EBus above is the real, working surface.
## Update an item
Works the same as Create above, with one difference: call `Update`/`SubmitItemUpdate` instead of
the create path, targeting an existing `PublishedFileId` (e.g. one you got back from the Create
flow, or from a search result).
**Unity C#**
```csharp
// Use the Workshop Item Editor Data to build up your item
WorkshopItemEditorData data = new();
data.title = "New Item";
data.description = "My first workshop item";
data.Content = new("C:\\MyModContent");
data.Preview = new("C:\\MyModPreviewImage.png");
// Call update when ready
data.Update(HandleCompleted, HandleUpdateStarted);
```
(`HandleCompleted`/`HandleUpdateStarted` are your own methods, same shapes as
`HandleCompletion`/`HandleUpdateStarted` above but with `WorkshopItemDataUpdateStatus` instead of
`WorkshopItemDataCreateStatus` for the completed callback.)
Code Free: use the Workshop Item Search template's "Edit" settings (auto-adds an Events block);
attach Input Fields for quick edits, or hand the item to a Workshop Item Editor for the full set
of fields. Running "Set Editor" loads the item into the editor's memory — note the preview image
and content folder path can't be reloaded from Steam, so you must set those yourself.
**Fixed relative to the live article** (Phase 1, unchanged this pass): same `Content`/`Preview`
casing fix as Create.
**Godot GDScript / C#**: same `Set*` calls as Create Item above, plus `UgcSubmitItemUpdate` /
`SubmitItemUpdate`, targeting an existing `published_file_id`/`publishedFileId` instead of a
freshly-created one via `UgcStartItemUpdate(app_id, existing_file_id)` /
`StartItemUpdate(appId, existingFileId)` directly (no `CreateItem` call needed for an update — that
step is only for brand-new items).
**O3DE C++**: identical to the "Create an item" C++ block above from step 2 onward — call
`StartItemUpdate(consumerAppId, existingPublishedFileId)` directly with the known ID instead of
waiting on a `CreateItem` callback, then the same `SetItemTitle`/`SetItemDescription`/`SetItemContent`/
`SetItemPreview`/`SubmitItemUpdate` sequence.
## List items (query)
**Unity C#**
```csharp
// Create a query to find the desired items
var query = UgcQuery.Get(EUGCQuery.k_EUGCQuery_RankedByTrend
, EUGCMatchingUGCType.k_EUGCMatchingUGCType_Items_ReadyToUse
, AppData.Me
, AppData.Me);
// Optionally filter on a search string
query.SetSearchText("Text to search");
// Run the query
query.Execute(HandleResults);
// Handle the results
void HandleResults(UgcQuery query)
{
// Iterate over the found items
foreach (var result in query.ResultsList)
{
// Do something with it
}
}
```
Code Free: the Workshop Item Search component's "Template" (spawned per result — attach a
Workshop Item component to display each one) and "Content" (parent transform, typically with a
Grid Layout) fields are the only required setup; trigger a search from a Button calling one of the
Search functions — Search All / Search Favourites / Search My Published / Search Subscribed.
Results are capped at 50 per page (Steam's own limit).
No discrepancies found in this block — `UgcQuery.Get(...)`, `SetSearchText`, `Execute`, and
`ResultsList` all match Toolkit's `UgcQuery` class exactly.
**Godot — Work In Progress, confirmed by source, unchanged this pass.** No query/search API exists
in either Foundation or Toolkit for browsing/searching published Workshop items by rank/type/tag —
grepped both entire source trees for `Query`/`Search`, found nothing beyond
`UgcGetSubscribedItems` (subscribed-only; see "Find installed content" below). Don't invent a
`UgcQuery`-style class for Godot — it doesn't exist yet.
**O3DE C++ — full query support exists, and it's Foundation-tier, not Toolkit.** Unlike Godot,
O3DE's `SteamUGCRequestBus` includes a real query surface: `CreateQueryAllUGCRequest`,
`CreateQueryUserUGCRequest`, `CreateQueryUGCDetailsRequest`, plus filter setters (`AddRequiredTag`,
`AddExcludedTag`, `SetMatchAnyTag`, `AddRequiredKeyValueTag`, `SetReturnLongDescription`,
`SetReturnMetadata`, `SetReturnChildren`, `SetReturnAdditionalPreviews`, `SetReturnTotalOnly`,
`SetLanguage`, `SetAllowCachedResponse`) and `SendQueryUGCRequest` (async, result via
`OnSteamUGCQueryCompleted` on `SteamUGCNotificationBus`), then `ReleaseQueryUGCRequest` when done.
This is genuinely more complete than either Unity or Godot's Foundation for this feature — but
again, **no Script Canvas nodeable wraps it**, so it's C++-only.
```cpp
Heathen::SteamUGCQueryHandle queryHandle = 0;
Heathen::SteamUGCRequestBus::BroadcastResult(
queryHandle, &Heathen::SteamUGCRequests::CreateQueryAllUGCRequest,
/*queryType (EUGCQuery)*/ 0, /*matchingFileType (EUGCMatchingUGCType)*/ 0,
creatorAppId, consumerAppId, /*page*/ 1);
Heathen::SteamUGCRequestBus::Broadcast(&Heathen::SteamUGCRequests::SetReturnMetadata, queryHandle, true);
Heathen::SteamCallHandle callHandle = 0;
Heathen::SteamUGCRequestBus::BroadcastResult(
callHandle, &Heathen::SteamUGCRequests::SendQueryUGCRequest, queryHandle);
Heathen::SteamUGCNotificationBus::Handler::BusConnect();
// In your OnSteamUGCQueryCompleted handler, once done:
Heathen::SteamUGCRequestBus::Broadcast(&Heathen::SteamUGCRequests::ReleaseQueryUGCRequest, queryHandle);
```
`queryType`/`matchingFileType` are raw `AZ::s32` casts of `EUGCQuery`/`EUGCMatchingUGCType` per the
header's own comments — pass the real Steamworks SDK enum values cast to `s32`, same as Unity's
`EUGCQuery`/`EUGCMatchingUGCType` enums.
## Find installed content (subscribed items)
**Unity C#**
```csharp
// Get a query for the subscribed items
var query = UgcQuery.GetSubscribed();
// Run the query
query.Execute(HandleResults);
// Handle the results
void HandleResults(UgcQuery query)
{
// Iterate over the found items
foreach (var result in query.ResultsList)
{
// Check if its installed and download if needed
if (!result.IsInstalled)
result.DownloadItem(true);
// Check if this item is downloading or going to download
if (result.IsDownloading
|| result.IsDownloadPending)
;// handle download in progress
// Monitor how much is downloaded
float percentComplete = result.DownloadCompletion;
// The location where the content is downloaded to
var folder = result.FolderPath;
}
}
```
Code Free: "Search Subscribed" on the Workshop Item Search component gets you the items the player
has installed; reading the downloaded content itself requires this C# code (no code-free path).
**Fixed relative to the live article** (Phase 1, unchanged this pass): the last line was a bare
`result.FolderPath;` property access with no assignment — not valid C# on its own — now assigned to
`var folder`.
Note: there is currently **no query/search API in Foundation or Toolkit for browsing/searching
published Workshop items by rank/type/tag** beyond what's shown in "List items" above — only items
the local user is subscribed to are queryable this way. (This is Unity/Godot-specific — O3DE does
have a full query API, see above.)
**Godot GDScript** — verified against `SteamApi.cpp` native implementation.
```gdscript
for id in SteamApi.UgcGetSubscribedItems():
var install_info := SteamApi.UgcGetItemInstallInfo(id)
# [size_on_disk, timestamp, path] -- only empty when Steam itself is not ready;
# check size_on_disk == 0 to actually detect "not installed yet"
if install_info.is_empty() or install_info[0] == 0:
SteamApi.UgcDownloadItem(id, true)
```
**Partial** — the building blocks exist, but there's no single "is it installed" boolean.
`UgcGetItemInstallInfo`'s native implementation (`SteamApi.cpp`) discards the underlying
`ISteamUGC::GetItemInstallInfo`'s own success/failure return value and always appends 3 elements to
the result array whenever Steam itself is ready — regardless of whether the item is actually
installed. So `is_empty()` only ever reflects "Steam not ready," never "item not installed"; check
`install_info[0] == 0` (`size_on_disk`) as well.
**Godot C#** — via the Toolkit facade.
```csharp
List<ulong> ids = API.UserGeneratedContent.Client.GetSubscribedItems();
foreach (var id in ids) {
string folder = API.UserGeneratedContent.Client.GetItemInstallInfo(id, out ulong sizeOnDisk, out bool legacyItem);
if (string.IsNullOrEmpty(folder))
API.UserGeneratedContent.Client.DownloadItem(id, true);
}
```
`GetItemInstallInfo` returns `string` (the install folder path, or `string.Empty`) per
`API.UserGeneratedContent.cs` — not `bool`. Using it as `!GetItemInstallInfo(...)` (as the live
article did before this pass's fix) would not compile.
**Fixed relative to the live article this pass** (both GDScript and C#, plus the GDScript "Partial"
prose paragraph): see the discrepancy report for full before/after.
**O3DE C++** — the underlying implementation is correct here (unlike Godot's), confirmed in
`FoundationSteamworks_UGC.cpp`: `GetItemInstallInfo`'s real `bool` return value is honoured and
out-params are only populated on success, so a straightforward bool check works as expected. New
content this pass — no O3DE section existed in the live article.
```cpp
// GetSubscribedItems fills outIds by reference and returns the count as AZ::u32
AZStd::vector<Heathen::SteamPublishedFileId> subscribedIds;
AZ::u32 count = 0;
Heathen::SteamUGCRequestBus::BroadcastResult(
count, &Heathen::SteamUGCRequests::GetSubscribedItems, subscribedIds, 64);
for (auto fileId : subscribedIds)
{
AZ::u64 sizeOnDisk = 0;
AZStd::string folder;
AZ::u32 timestamp = 0;
bool installed = false;
Heathen::SteamUGCRequestBus::BroadcastResult(
installed, &Heathen::SteamUGCRequests::GetItemInstallInfo, fileId, sizeOnDisk, folder, timestamp);
if (!installed)
{
bool downloadStarted = false;
Heathen::SteamUGCRequestBus::BroadcastResult(
downloadStarted, &Heathen::SteamUGCRequests::DownloadItem, fileId, true);
}
}
```
`GetSubscribedItems(AZStd::vector<SteamPublishedFileId>& outIds, AZ::u32 maxEntries)` and
`GetItemInstallInfo(SteamPublishedFileId, AZ::u64&, AZStd::string&, AZ::u32&)` signatures both
confirmed exactly against `SteamUGCRequestBus.h`; the `bool` return semantics confirmed correct
(unlike Godot) against `FoundationSteamworks_UGC.cpp`'s actual implementation.
---
## If you're stuck
- Full human-readable article (all prose, screenshots, Code-Free inspector field docs this file
compresses out): https://heathen.group/kb/workshop/
- This file is generated from that article's raw content plus new O3DE material, with 8 verified
corrections/additions against Foundation/Toolkit source across Unity (4, Phase 1) and Godot (4,
Phase 2) — see the discrepancy report; if something here looks wrong, treat the live article +
actual source as the fallback truth, not this file's memory.
---
Back to the full Steam agent index: https://heathen.group/agent-ref-steam-index/