BuildGraph is the build-automation system that ships inside Unreal Engine. Most people meet it through Horde or a build farm, so it’s easy to assume you need all of that to use it. You don’t. RunUAT will run a BuildGraph script directly on your own machine, and for a solo developer that turns “build and package the game” into a single command you can run, script, or schedule with no server anywhere in sight.

I run our studio’s builds through Horde, but the scripts underneath are just BuildGraph, and the same scripts work perfectly well on their own. If you’re one person shipping a game, this is the cheapest automation you’ll ever set up, because it’s already installed.

What BuildGraph actually is

A BuildGraph script is an XML file describing a graph of nodes, where each node is a sequence of tasks: compile a target, cook content, stage a build, run a command, copy files, zip something up. RunUAT reads the script and executes the nodes in dependency order.

Horde is one way to run that graph, spreading nodes across a farm of machines. But the default executor is simply your local machine, one node after another. The important consequence: the same script runs in both places unchanged. Anything you automate on your laptop today scales up to CI later for free, because there’s nothing to port.

But the editor already has “Package Project”

It does, and for a one-off it’s fine. It’s worth knowing what that menu item actually is, though: when you pick Platforms > Windows > Package Project, the editor shells out to the same BuildCookRun this script calls. So running BuildGraph yourself isn’t a heavier or more obscure route to a packaged build, it’s the same machinery with you holding the controls instead of a menu.

Holding the controls is what buys you:

  • It runs headless and doesn’t tie up your editor. Packaging from the menu needs the editor open and keeps it busy for the entire cook. The command line needs no editor at all, so you can keep working, keep it closed, or run the build overnight while you’re asleep.
  • The settings are explicit and version-controlled. The menu’s behaviour comes from options spread through Project Settings, easy to change by accident and invisible in a diff. In the script, every flag that shapes the build sits in one file next to the game, the same for everyone and reviewable whenever it changes.
  • It doesn’t stop at “packaged.” The menu’s job ends when the folder appears. A script keeps going, and because it’s a command rather than a click it can be zipped, uploaded, scheduled, or hooked into a commit. A menu item can be none of those.

None of this makes the button wrong. It makes it a step you repeat by hand forever, where the script is that step written down once.

A script that builds and packages your game

Here’s a complete graph that compiles, cooks, stages and packages a Win64 build. It’s Unreal’s BuildCookRun pipeline wrapped in a single node:

<?xml version='1.0' ?>
<BuildGraph xmlns="http://www.epicgames.com/BuildGraph"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://www.epicgames.com/BuildGraph ../../Engine/Build/Graph/Schema.xsd">

  <Option Name="ProjectFile"   DefaultValue="" Description="Path to the .uproject"/>
  <Option Name="GameTarget"    DefaultValue="" Description="Game/client target name (from *.Target.cs)"/>
  <Option Name="Configuration" Restrict="Development|Test|Shipping" DefaultValue="Shipping" Description="Build configuration"/>
  <Option Name="Platform"      DefaultValue="Win64" Description="Target platform"/>
  <Option Name="OutputDir"     DefaultValue="$(RootDir)/LocalBuilds/$(GameTarget)" Description="Where the packaged build lands"/>

  <Agent Name="Package Agent" Type="Win64">
    <Node Name="Package Game">
      <Log Message="Packaging $(GameTarget) ($(Configuration)) for $(Platform)"/>

      <!-- Build the target, cook content, stage, pak, and archive a runnable build -->
      <Command Name="BuildCookRun"
               Arguments="-project=&quot;$(ProjectFile)&quot; -target=$(GameTarget)
                          -platform=$(Platform) -clientconfig=$(Configuration)
                          -build -cook -stage -pak -archive
                          -archivedirectory=&quot;$(OutputDir)&quot; -nop4 -utf8output"/>

      <!-- Wrap the staged build into a single distributable zip -->
      <Zip FromDir="$(OutputDir)/$(Platform)"
           ZipFile="$(OutputDir)/$(GameTarget)-$(Configuration)-$(Platform).zip"/>
    </Node>
  </Agent>

  <Aggregate Name="Package" Requires="Package Game"/>
</BuildGraph>

The Option elements are the inputs, filled in when you run it. The BuildCookRun command is the workhorse: -build compiles the target, -cook cooks the content for the platform, -stage -pak -archive assemble a runnable, paked build into OutputDir, and -nop4 tells it not to expect Perforce. The Zip task at the end folds the staged output into one archive you can hand to a playtester or upload. Save it as something like Build/PackageGame.xml next to your project.

Running it

This is the part people are surprised by. There’s no Horde, no daemon, no config. You call RunUAT and point it at the script:

Engine\Build\BatchFiles\RunUAT.bat BuildGraph ^
  -Script="MyGame\Build\PackageGame.xml" ^
  -Target="Package" ^
  -set:ProjectFile="MyGame\MyGame.uproject" ^
  -set:GameTarget=MyGame ^
  -set:Configuration=Shipping

RunUAT parses the graph, runs the Package Game node on your machine, and leaves a packaged build (and a zip) in LocalBuilds/MyGame. -Target names the aggregate to build, and each -set: fills in one of the script’s options.

You won’t want to retype that, so wrap it. A small batch file next to your project makes packaging a double-click:

@echo off
setlocal
rem Point this at your engine install (launcher or source build)
set ENGINE=C:\Program Files\Epic Games\UE_5.5\Engine

"%ENGINE%\Build\BatchFiles\RunUAT.bat" BuildGraph ^
  -Script="%~dp0Build\PackageGame.xml" ^
  -Target="Package" ^
  -set:ProjectFile="%~dp0MyGame.uproject" ^
  -set:GameTarget=MyGame ^
  -set:Configuration=Shipping

echo.
echo Build finished. Output in LocalBuilds\MyGame
pause

%~dp0 is the folder the batch file lives in, so the paths stay correct wherever you check the project out. Now packaging a shippable build is one action: run package.bat. Put it in a scheduled task for a nightly build, or a git hook, and it’s automation you never think about again.

Why it’s worth it, even solo

Beyond the direct comparison with the menu, two things make this pay off over time:

  • It scales without a rewrite. This is the exact same tool, and if you want the exact same script, that a studio runs across a farm. The day you outgrow your laptop and want a build machine or CI, you point Horde at the script you already have. Nothing about the local version is throwaway.
  • It’s already installed and it’s yours. Nothing to download, no service to run, no subscription. It came with the engine, and the automation lives in your repo next to the game.

Extending it

Because tasks just run in sequence, growing the pipeline is mechanical. Say you want to push each build to itch.io. itch’s uploader, butler, is a single command, so a Spawn task does it:

<Spawn Exe="butler"
       Arguments="push &quot;$(OutputDir)/$(GameTarget)-$(Configuration)-$(Platform).zip&quot; you/your-game:windows"/>

Drop that after the Zip task and package.bat now builds, packages, and publishes in one go. The same pattern covers anything with a command-line tool behind it, which on Windows is most things.

That’s the whole pitch. BuildGraph tends to get filed under “CI stuff for big teams,” but it’s really just a build-automation language that happens to ship with Unreal, and it’s as useful to one person as to a hundred. If you’re hand-running BuildCookRun with a different set of flags each time, this is a good afternoon’s investment. Let me know if you set something up with it.