---
title: "Workshop and user content"
description: "Publish, discover, and download content through Steam Workshop."
seo:
  noindex: true
---

{/* Copyright (c) 2026 Betide Studio. All Rights Reserved. */}

Configure Workshop for your [Steam app](/integrations/steam/setup). Decide which content formats your game accepts and how it loads them; Workshop stores and distributes files but does not mount Unreal content for you.

## Query content

Browse the first page of items ranked by votes. Create `WorkshopQuery` (SIK UGC Query Handle) and `WorkshopQueryPending` (Boolean, false).

**Blueprint**

<BlueprintViewer title="Browse Workshop items" src="/blueprints/crossplay-steam-query-workshop.txt" />

**C++**

Game module dependency: `SteamIntegrationKit`.

`WorkshopBrowserGameInstance.h`

```cpp
#pragma once
#include "Engine/GameInstance.h"
#include "Nodes/UGC/SIK_SendQueryUGCRequest_AsyncFunction.h"
#include "WorkshopBrowserGameInstance.generated.h"

UCLASS()
class UWorkshopBrowserGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    void BrowseWorkshop(FSIK_AppId AppId);
    virtual void Shutdown() override;

  private:
    UPROPERTY()
    TObjectPtr<USIK_SendQueryUGCRequest_AsyncFunction> Request;
    FSIK_UGCQueryHandle Query;
    bool bQueryPending = false;
    UFUNCTION()
    void QueryFinished(FSIK_UGCQueryHandle Handle, TEnumAsByte<ESIK_Result> Result, int32 Returned,
                       int32 Total, bool bCached);
};
```

`WorkshopBrowserGameInstance.cpp`

```cpp
#include "WorkshopBrowserGameInstance.h"
#include "Nodes/UGC/SIK_UGCLibrary.h"
#include "SteamIntegrationKit.h"

void UWorkshopBrowserGameInstance::BrowseWorkshop(FSIK_AppId AppId)
{
    if (bQueryPending)
    {
        UE_LOG(LogSIK, Warning, TEXT("A Workshop query is already running"));
        return;
    }
    Query = USIK_UGCLibrary::CreateQueryAllUGCRequest(UGCQuery_RankedByVote,
                                                      UGCMatchingUGCType_Items, AppId, AppId, 1);
    bQueryPending = true;
    Request = USIK_SendQueryUGCRequest_AsyncFunction::SendQueryUGCRequest(Query);
    Request->RegisterWithGameInstance(this);
    Request->OnSuccess.AddDynamic(this, &UWorkshopBrowserGameInstance::QueryFinished);
    Request->OnFailure.AddDynamic(this, &UWorkshopBrowserGameInstance::QueryFinished);
    Request->Activate();
}

void UWorkshopBrowserGameInstance::QueryFinished(FSIK_UGCQueryHandle Handle,
                                                 TEnumAsByte<ESIK_Result> Result, int32 Returned,
                                                 int32 Total, bool bCached)
{
    if (!bQueryPending)
        return;
    if (Result == ESIK_Result::ResultOK)
    {
        for (int32 Index = 0; Index < Returned; ++Index)
        {
            FSIK_SteamUGCDetails Details;
            if (USIK_UGCLibrary::GetQueryUGCResult(Query, Index, Details) &&
                Details.Result == ESIK_Result::ResultOK)
                UE_LOG(LogSIK, Log, TEXT("%s"), *Details.Title);
            else
                UE_LOG(LogSIK, Warning, TEXT("Workshop item details are unavailable"));
        }
    }
    else
        UE_LOG(LogSIK, Warning, TEXT("Workshop query failed: %d"), int32(Result.GetValue()));
    USIK_UGCLibrary::ReleaseQueryUGCRequest(Query);
    bQueryPending = false;
    Request = nullptr;
}

void UWorkshopBrowserGameInstance::Shutdown()
{
    if (Request)
        Request->Cancel();
    Super::Shutdown();
}
```

Pages start at **1**; result indexes start at **0**. Apply filters and **Set SIK Return Metadata** before sending the query. Read all required details before releasing its handle. Use **Create SIK Query UGC Details Request** when you already know the published file IDs.

## Subscribe and download

Download a known item and read its folder once it is installed and up to date. The watcher reports progress once per second and stops after five minutes. It does not change subscriptions or cancel Steam’s download.

For the installation graph, create `InstallItem` (SIK Published File Id), `InstallActive` (Boolean, false), `InstallTimer` (Timer Handle), and `PollsRemaining` (Integer) in an Actor.

**Blueprint**

<BlueprintViewer title="Subscribe and unsubscribe from an item" src="/blueprints/crossplay-steam-workshop-subscription.txt" />

<BlueprintViewer title="Download and watch Workshop installation" src="/blueprints/crossplay-steam-workshop-install.txt" />

**C++**

```cpp
#include "Nodes/UGC/SIK_UGCLibrary.h"
#include "Nodes/UGC/SIK_SubscribeItem_AsyncFunction.h"
#include "Nodes/UGC/SIK_UnsubscribeItem_AsyncFunction.h"

bool DownloadWorkshopItem(FSIK_PublishedFileId Item)
{
    return USIK_UGCLibrary::DownloadItem(Item, false);
}

bool SubscribeWorkshopItem(FSIK_PublishedFileId Item, const FScriptDelegate &Ready,
                           const FScriptDelegate &Failed)
{
    auto *Request = USIK_SubscribeItem_AsyncFunction::SubscribeItem(Item);
    if (!Request)
        return false;
    Request->OnSuccess.Add(Ready);
    Request->OnFailure.Add(Failed);
    Request->Activate();
    return true;
}

bool UnsubscribeWorkshopItem(FSIK_PublishedFileId Item, const FScriptDelegate &Ready,
                             const FScriptDelegate &Failed)
{
    auto *Request = USIK_UnsubscribeItem_AsyncFunction::UnsubscribeItem(Item);
    if (!Request)
        return false;
    Request->OnSuccess.Add(Ready);
    Request->OnFailure.Add(Failed);
    Request->Activate();
    return true;
}
```

Subscription handlers take `(TEnumAsByte<ESIK_Result>, FSIK_PublishedFileId)`.

`WorkshopDownloadActor.h`

```cpp
#pragma once
#include "GameFramework/Actor.h"
#include "Nodes/UGC/SIK_UGCSubsystem.h"
#include "WorkshopDownloadActor.generated.h"

UCLASS()
class AWorkshopDownloadActor : public AActor
{
    GENERATED_BODY()
  public:
    void DownloadWorkshopItem(FSIK_PublishedFileId Item);
    void StopWatchingWorkshopDownload();
    virtual void EndPlay(const EEndPlayReason::Type Reason) override;

  private:
    UPROPERTY()
    TObjectPtr<USIK_UGCSubsystem> UGC;
    FSIK_PublishedFileId InstallItem;
    FTimerHandle InstallTimer;
    int32 PollsRemaining = 0;
    bool bInstallActive = false;
    void PollWorkshopDownload();
    UFUNCTION()
    void DownloadFinished(TEnumAsByte<ESIK_Result> Result, FSIK_PublishedFileId Item,
                          FSIK_AppId App);
};
```

`WorkshopDownloadActor.cpp`

```cpp
#include "WorkshopDownloadActor.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "Nodes/UGC/SIK_UGCLibrary.h"
#include "Nodes/Utils/SIK_UtilsLibrary.h"
#include "SteamIntegrationKit.h"
#include "TimerManager.h"

void AWorkshopDownloadActor::DownloadWorkshopItem(FSIK_PublishedFileId Item)
{
    if (bInstallActive)
    {
        UE_LOG(LogSIK, Warning, TEXT("A Workshop download is already being watched"));
        return;
    }
    UGameInstance *Instance = GetGameInstance();
    UGC = Instance ? Instance->GetSubsystem<USIK_UGCSubsystem>() : nullptr;
    if (!UGC)
    {
        UE_LOG(LogSIK, Warning, TEXT("Workshop subsystem is unavailable"));
        return;
    }
    InstallItem = Item;
    UGC->OnDownloadItemResult.AddUniqueDynamic(this, &AWorkshopDownloadActor::DownloadFinished);
    bInstallActive = true;
    PollsRemaining = 300;
    if (!USIK_UGCLibrary::DownloadItem(Item, false))
    {
        StopWatchingWorkshopDownload();
        UE_LOG(LogSIK, Warning, TEXT("Steam could not queue this download"));
        return;
    }
    GetWorldTimerManager().SetTimer(InstallTimer, this,
                                    &AWorkshopDownloadActor::PollWorkshopDownload, 1.0f, true);
    PollWorkshopDownload();
}

void AWorkshopDownloadActor::PollWorkshopDownload()
{
    if (!bInstallActive)
        return;
    const int32 State = USIK_UGCLibrary::GetItemState(InstallItem);
    // Installed (4), without NeedsUpdate (8), Downloading (16), or DownloadPending (32).
    if ((State & 60) == 4)
    {
        int64 Size = 0;
        FString Folder;
        int32 Timestamp = 0;
        const bool bReadable =
            USIK_UGCLibrary::GetItemInstallInfo(InstallItem, Size, Folder, Timestamp);
        StopWatchingWorkshopDownload();
        if (bReadable)
            UE_LOG(LogSIK, Log, TEXT("%s"), *Folder);
        else
            UE_LOG(LogSIK, Warning, TEXT("Steam could not read the installed folder"));
        return;
    }
    if (--PollsRemaining <= 0)
    {
        StopWatchingWorkshopDownload();
        UE_LOG(LogSIK, Warning,
               TEXT("Stopped waiting after five minutes; Steam may continue downloading"));
        return;
    }
    int64 Downloaded = 0;
    int64 Total = 0;
    if (USIK_UGCLibrary::GetItemDownloadInfo(InstallItem, Downloaded, Total))
        UE_LOG(LogSIK, Log, TEXT("Download: %lld / %lld bytes"), Downloaded, Total);
}

void AWorkshopDownloadActor::DownloadFinished(TEnumAsByte<ESIK_Result> Result,
                                              FSIK_PublishedFileId Item, FSIK_AppId App)
{
    if (!bInstallActive || Item.PublishedFileId != InstallItem.PublishedFileId ||
        App.AppID != USIK_UtilsLibrary::GetAppID().AppID)
        return;
    if (Result == ESIK_Result::ResultOK)
        PollWorkshopDownload();
    else
    {
        StopWatchingWorkshopDownload();
        UE_LOG(LogSIK, Warning, TEXT("Workshop download failed: %d"), int32(Result.GetValue()));
    }
}

void AWorkshopDownloadActor::StopWatchingWorkshopDownload()
{
    bInstallActive = false;
    GetWorldTimerManager().ClearTimer(InstallTimer);
    if (UGC)
        UGC->OnDownloadItemResult.RemoveDynamic(this, &AWorkshopDownloadActor::DownloadFinished);
}

void AWorkshopDownloadActor::EndPlay(const EEndPlayReason::Type Reason)
{
    StopWatchingWorkshopDownload();
    Super::EndPlay(Reason);
}
```

A successful download request means queued, not installed. Use **Get SIK Item Download Info** for progress and check item state before loading files: an installed copy can still need an update. Subscription and installation are separate; **Unsubscribe SIK Item** is not a replacement for unloading content already in use.

Validate the installed content's format and version before loading it. Do not execute arbitrary downloaded code or trust paths supplied inside a user's content package.

## Publish or update an item

Create the item once and persist its **Published File ID**. Updates use that ID; creating a new item for every edit produces separate Workshop entries. Create `PublishedItem` (SIK Published File Id), `WorkshopUpdate` (SIK UGC Update Handle), and `WorkshopUploading` (Boolean, false). Store the returned Published File ID in your own save data; the example variable lasts only for the current run.

**Blueprint**

<Accordion>
  <AccordionItem title="Create a Workshop entry">
<BlueprintViewer title="Create a Workshop entry" src="/blueprints/crossplay-steam-create-workshop-item.txt" />
  </AccordionItem>
  <AccordionItem title="Upload a revision and read progress">
<BlueprintViewer title="Upload a revision and read progress" src="/blueprints/crossplay-steam-upload-workshop.txt" />
  </AccordionItem>
</Accordion>

**C++**

```cpp
#include "Nodes/UGC/SIK_UGCLibrary.h"
#include "Nodes/UGC/SIK_SubmitItemUpdate_AsyncFunction.h"
#include "Nodes/UGC/SIK_CreateItem_AsyncFunction.h"

bool CreateWorkshopEntry(FSIK_AppId AppId, const FScriptDelegate &Created,
                         const FScriptDelegate &Failed)
{
    auto *Request = USIK_CreateItem_AsyncFunction::CreateItem(AppId, WorkshopFileTypeCommunity);
    if (!Request)
        return false;
    Request->OnSuccess.Add(Created);
    Request->OnFailure.Add(Failed);
    Request->Activate();
    return true;
}

bool UploadWorkshopRevision(FSIK_AppId AppId, FSIK_PublishedFileId Item,
                            const FString &ContentFolder, const FString &Title,
                            const FString &PreviewFile, const FString &ChangeNote,
                            FSIK_UGCUpdateHandle &OutUpdate, const FScriptDelegate &OnUploaded,
                            const FScriptDelegate &OnFailed)
{
    const auto Update = USIK_UGCLibrary::StartItemUpdate(AppId, Item);
    OutUpdate = Update;
    if (!USIK_UGCLibrary::SetItemContent(Update, ContentFolder) ||
        !USIK_UGCLibrary::SetItemTitle(Update, Title) ||
        !USIK_UGCLibrary::SetItemPreview(Update, PreviewFile))
        return false;
    auto *Request = USIK_SubmitItemUpdate_AsyncFunction::SubmitItemUpdate(Update, ChangeNote);
    if (!Request)
        return false;
    Request->OnSuccess.Add(OnUploaded);
    Request->OnFailure.Add(OnFailed);
    Request->Activate();
    return true;
}
```

Creation handlers take `(TEnumAsByte<ESIK_Result>, FSIK_PublishedFileId, bool bUserNeedsToAcceptWorkshopLegalAgreement)`. Upload handlers take `(TEnumAsByte<ESIK_Result>, bool bUserNeedsToAcceptWorkshopLegalAgreement)`. The helper returns dispatch status; handle completion through its success and failure delegates. Keep one upload active per saved update handle, and clear your uploading flag on either completion path.

Pass the absolute folder containing the files to upload. Keep it available until completion. Set preview, description, tags, and metadata on the update handle before submitting. Call the progress event from your upload UI or a timer. In C++, call `GetItemUpdateProgress(OutUpdate, BytesProcessed, BytesTotal)` only while uploading and read the byte counts only when the returned status is not `ItemUpdateStatusInvalid`. Use **Get SIK Item Update Progress** while the upload is active; create a new update handle for the next revision.

If creation or upload says the user must accept the Workshop legal agreement, direct them to Steam's agreement flow. Do not report the item as publicly available until that requirement and its visibility settings are satisfied. Follow [Valve's Workshop implementation guide](https://partner.steamgames.com/doc/features/workshop/implementation).

<Accordion>
  <AccordionItem title="Ownership, visibility, and failures">

Publishing requires the signed-in account to have permission to update the item. Creator App ID and consumer App ID can differ when a separate tool publishes content for a game; use the intended pair when querying.

Handle upload, download, and query failures separately. An empty successful query can reflect visibility or filters, not a failed Steam connection. Do not discard a saved Published File ID after a transient upload failure.

Use **Delete SIK Item** only for a deliberate removal of a published entry. Removing a subscription, removing a favorite, and deleting a published item are different actions.

  </AccordionItem>
</Accordion>
