For skeptics. This document walks through exactly what happens when Claude builds a Fortnite Creative game autonomously using UEFN Toolbelt — every tool call, every output, and the hard limits of what's possible today.
Not "AI suggests code you copy-paste." Not "AI fills in a template you prompt manually."
The loop is:
- Claude reads your live level — transforms, classes, device labels, all 521 actors
- Claude reasons about the level — identifies game-logic devices, infers a game concept
- Claude writes valid Verse —
@editablerefs wired to real device labels, full event logic - Claude deploys it — writes the
.versefile directly to the project's Verse directory - You click Build Verse — it compiles
The only human action in steps 1–4 is pasting a single Python line into the UEFN console.
| Tool | What it does |
|---|---|
world_state_export |
Snapshots every actor in the level to docs/world_state.json — label, class, transforms, readable properties |
verse_gen_game_skeleton |
Generates a full creative_device Verse skeleton from a device name |
verse_write_file |
Writes any Verse content directly to the project's Verse source directory (not the engine — the real project path) |
verse_find_project_path |
Auto-detects the correct Verse folder by walking up from __file__, avoiding the unreal.Paths.project_dir() bug that returns the FortniteGame engine dir |
- 521 total actors in level
- 40 Creative/Verse devices identified
- 6187 bytes of Verse generated
- Build result:
VerseBuild: SUCCESS— zero errors, first compile
tb.run("world_state_export")Output:
[TOOLBELT] world_state_export: capturing 521 actors...
[TOOLBELT] ✓ World state saved: Saved/UEFN_Toolbelt/world_state.json
[TOOLBELT] ✓ Auto-synced to repo: docs/world_state.json
{'status': 'ok', 'count': 521}
From world_state.json, Claude identified:
| Class | Label | Location |
|---|---|---|
FortCreativeTimerDevice |
Timer Device | (4, -784, 488) |
FortCreativeTimerObjective |
Timed Objective | (4, -784, 488) |
FortCreativeRoundSettings |
Round Settings | (-280, -796, 596) |
BuildingProp_CaptureArea |
Capture Area | (112, -100, 0) |
BuildingProp_CaptureItemSpawner |
Capture Item Spawner | (428, -108, 0) |
BuildingProp_CaptureItemSpawner |
Capture Item Spawner2 | (108, -668, 300) |
BuildingProp_DeimosSpawner |
Creature Spawner | (584, -1276, 36) |
BuildingProp_CreatureManager |
Creature Manager | (16, -696, 0) |
BuildingProp_CreaturePlacer |
Creature Placer | (176, -500, 0) |
BuildingProp_SwitchDevice |
Button | (7119, -1000, 115) |
BuildingProp_SwitchDevice |
Button2 | (72, -1000, 552) |
FortCreativeLockDevice |
Conditional Button | (9751, 1469, 101) |
FortCreativeLockDevice |
Conditional Button2 | (92, -996, 632) |
BuildingProp_LockDevice |
Lock Device | (36, -992, 576) |
FortCreativeTeleporter × 14 |
Teleporter–Teleporter14 | (various) |
CreativeAudioMixerDevice |
Audio Mixer | (608, -480, 0) |
FortWeaponModStationBase |
Weapon Mod Bench | (28, -1048, 344) |
FortAthenaSplineZipline |
Zipline | (-572, -268, 604) |
Game concept inferred by Claude: Capture & Defend — players hold a capture area while creature waves attack; timer counts down the round; buttons control doors and conditional gates.
tb.run("verse_write_file",
filename="device_api_game_manager.verse",
content=verse_code,
overwrite=True)Output:
[TOOLBELT] ✓ Written: Device_API_Mapping\Verse\device_api_game_manager.verse (6187 bytes)
{'status': 'ok', 'bytes': 6187}
VerseBuild: Compilation complete.
VerseBuild: Linking complete.
VerseBuild: SUCCESS -- Build complete.
# device_api_game_manager.verse
# AUTO-GENERATED by UEFN Toolbelt (world_state_export)
# Level: Device_API_Mapping Actors: 521 Devices: 40+
# Generated: 2026-03-22
using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Fortnite.com/Playspaces }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
device_api_game_manager := class(creative_device):
@editable GameTimer : timer_device = timer_device{}
@editable TimedObjective : timed_objective_device = timed_objective_device{}
@editable RoundSettings : round_settings_device = round_settings_device{}
@editable CaptureArea : capture_area_device = capture_area_device{}
@editable CaptureItemSpawner : item_spawner_device = item_spawner_device{}
@editable CaptureItemSpawner2 : item_spawner_device = item_spawner_device{}
@editable CreatureSpawner : guard_spawner_device = guard_spawner_device{}
@editable CreatureManager : creature_manager_device = creature_manager_device{}
@editable CreaturePlacer : creature_placer_device = creature_placer_device{}
@editable Button : button_device = button_device{}
@editable Button2 : button_device = button_device{}
@editable ConditionalButton : conditional_button_device = conditional_button_device{}
@editable ConditionalButton2 : conditional_button_device = conditional_button_device{}
@editable LockDevice : lock_device = lock_device{}
@editable Teleporter11 : teleporter_device = teleporter_device{}
@editable Teleporter10 : teleporter_device = teleporter_device{}
@editable Teleporter12 : teleporter_device = teleporter_device{}
@editable Teleporter13 : teleporter_device = teleporter_device{}
@editable Teleporter14 : teleporter_device = teleporter_device{}
@editable AudioMixer : audio_player_device = audio_player_device{}
@editable WeaponModBench : weapon_mod_station_base = weapon_mod_station_base{}
OnBegin<override>()<suspends> : void =
GameTimer.Start()
spawn { WatchCaptureArea() }
spawn { WatchCreatureWaves() }
Button.InteractedWithEvent.Subscribe(OnButtonPressed)
Button2.InteractedWithEvent.Subscribe(OnButton2Pressed)
GameTimer.SuccessEvent.Subscribe(OnTimerComplete)
TimedObjective.ObjectiveCompleteEvent.Subscribe(OnObjectiveComplete)
WatchCaptureArea()<suspends> : void =
loop:
CaptureEvent := CaptureArea.TeamChangedEvent.Await()
CaptureItemSpawner.Spawn()
CaptureItemSpawner2.Spawn()
CreatureSpawner.Spawn()
WatchCreatureWaves()<suspends> : void =
loop:
CreatureManager.Enable()
Sleep(30.0)
OnButtonPressed(Agent : agent) : void =
LockDevice.Unlock(Agent)
OnButton2Pressed(Agent : agent) : void =
ConditionalButton.Activate(Agent)
OnTimerComplete(Agent : agent) : void = EndRound()
OnObjectiveComplete(Agent : agent) : void = EndRound()
EndRound() : void =
GameTimer.Stop()
CreatureManager.Disable()
CreatureSpawner.Despawn()
AudioMixer.Stop()
world_state_export answers: "What's in this level?"
device_catalog_scan answers: "What could be in any level?"
This is the difference between an AI that reacts to what you've built and an AI that can design from scratch. By scanning the full UEFN Asset Registry for every Blueprint class matching Creative device keywords across all Fortnite packages, Claude gets a complete palette of every device available — whether or not any of them are in the current level.
tb.run("device_catalog_scan")
# → Scanned 24,926 Blueprint assets across /Fortnite, /Game, /FortniteGame
# → 4,698 Creative devices identified across 35 categories
# → Saves to docs/device_catalog.jsonLive run results — March 22, 2026 (first run ever):
| Category | Devices | Category | Devices |
|---|---|---|---|
| Prop | 1,573 | Spawner | 94 |
| Creative | 504 | Race | 75 |
| Stat | 486 | Audio | 66 |
| Lock | 463 | Beacon | 47 |
| Camera | 446 | Pad | 43 |
| NPC | 173 | Tracker | 33 |
| Round | 144 | Chest | 32 |
| Device | 102 | Zipline | 31 |
| Gate | 95 | Manager | 29 |
| Mutator | 29 | Score | 28 |
| Supply | 25 | Trigger | 24 |
| Guard | 24 | Zone | 23 |
| Button | 22 | Barrier | 19 |
| Ammo | 16 | Pulse | 13 |
| Creature | 9 | Timer | 8 |
| Capture | 7 | Teleporter | 5 |
| Switch | 3 | FortAthena | 1 |
Total: 4,698 devices. 24,926 Blueprint assets scanned. 35 categories.
What this unlocks:
- Claude can now say "this level needs a Score Manager — here's the exact asset path"
- Claude can compare what's in the level vs what's available to place
- Claude can generate Verse wiring for devices that don't exist in the level yet, then tell you exactly which Content Browser asset to drag in
The discovery that almost broke it:
asset.object_path and asset.asset_class are deprecated in UEFN 40.00's Asset Registry API.
The tool's original try/except: continue block was silently skipping all 24,926 assets because
the deprecated property threw on every single call. The fix was to split the try/except:
make asset_name the only required field, use get_full_name() and asset_class_path for
the rest with individual fallbacks. Documented in UEFN_QUIRKS.md.
How it works under the hood:
ar = unreal.AssetRegistryHelpers.get_asset_registry()
flt = unreal.ARFilter(
package_paths=["/Game"], # /Fortnite and /FortniteGame are sandboxed (0 results)
class_names=["Blueprint", "BlueprintGeneratedClass"],
recursive_paths=True,
)
assets = ar.get_assets(flt) # 24,926 Blueprint assets
# Filter by 35 device-hint keywords → 4,698 matches
# Use get_full_name() not object_path → deprecated API fix
# → device_catalog.jsonResults are grouped by category and saved to docs/device_catalog.json — git-tracked so
Claude has the full palette available in every future session without re-scanning.
Every other "AI-assisted UEFN" tool requires you to tell the AI what's in your level.
world_state_export inverts this — Claude asks the level directly. The tool iterates every
actor via EditorActorSubsystem, reads its transforms and all accessible properties, and
writes a structured JSON. Claude can then reason over 521 actors in seconds.
unreal.Paths.project_dir() returns the FortniteGame engine directory
(../../../FortniteGame/), not your project. Writing there would corrupt Epic's game files.
The fix is _find_uefn_project_root() — walk up from __file__ until hitting the Content
folder, then use its parent. This is the actual UEFN project root regardless of where UEFN
is installed. Documented in UEFN_QUIRKS.md.
Fortnite V2 Creative devices (Timer, Capture Area, Score Manager, etc.) store their game-logic
settings (duration, team_index, channel) as Verse @editable properties — not UPROPERTYs.
set_editor_property raises an exception on these. getattr returns nothing.
The architecturally correct solution (and what Epic intends) is exactly what this generator does:
create a Verse creative_device that holds @editable references to the target devices and
configures them at on_begin. The generator IS the solution, not a workaround.
Full breakdown: UEFN_QUIRKS.md — Quirk #19.
| Limit | Reason | Workaround |
|---|---|---|
| Cannot trigger Verse compiler from Python | Epic has not exposed a BuildVerseCode Python API |
Click Verse → Build Verse Code manually |
| Cannot set V2 device game-logic properties via Python | Stored as Verse @editable, not UPROPERTYs |
Generate Verse code that sets them in OnBegin |
Cannot call runtime-only device methods (e.g. timer_start(player)) from editor |
Requires a live player reference — editor session has none | Call from Verse at game runtime |
Cannot read Verse @editable property values set in the editor UI |
Not exposed to Python layer | Read via world_state_export (base props only) |
When Epic exposes a Python BuildVerseCode API, the loop becomes fully headless — no human
clicks required at all.
This is the piece that makes the pipeline industrial. After any Verse build failure, Claude calls one tool and gets back everything it needs to fix the code and redeploy.
# After user clicks Build Verse and errors appear:
result = tb.run("verse_patch_errors")
# What Claude gets back:
{
"build_status": "FAILED",
"error_count": 3,
"errors": [
{"file": "game_manager.verse", "line": 42, "col": 7,
"message": "identifier 'capture_area_device' not found"},
{"file": "game_manager.verse", "line": 87, "col": 4,
"message": "no overload of 'Subscribe' takes 1 argument"},
],
"files": {
"game_manager.verse": "...full current content of the file..."
},
"next_step": "Fix 3 errors in the files listed, then call verse_write_file(overwrite=True) and rebuild."
}
# Claude reads errors + file, generates fix:
tb.run("verse_write_file", filename="game_manager.verse",
content=fixed_content, overwrite=True)
# User clicks Build Verse again → repeat until:
# {"build_status": "SUCCESS", "error_count": 0}- Finds the most recent
.logfile in UEFN'sSaved/Logs/directory - Scans every line for the Verse error pattern:
path/file.verse(line:col): error message - Also scans for
VerseBuild SUCCESS/FAILEDsummary lines - Deduplicates errors (same file+line+message appear multiple times in the log)
- For each erroring filename, walks the project Verse directory to find and read the file
- Returns errors + file contents +
next_stepinstruction in one structured dict
| Error message | Root cause | Fix |
|---|---|---|
identifier 'X' not found |
Wrong Verse device type name | Look up correct type in api_verse_get_schema |
no overload of 'Subscribe' takes 1 argument |
Event handler wrong signature | Add (Agent : agent) parameter |
'X' is not a member of 'Y' |
Method doesn't exist on device | Run api_crawl_selection to find real methods |
expected expression |
Indentation or syntax error in generated code | Fix whitespace / missing colon |
type mismatch: expected X, got Y |
Wrong type passed to method | Cast or use correct Verse type |
TODAY (fully operational):
Phase 0: scaffold_generate, organize_assets
Phase 1: world_state_export (521 actors), device_catalog_scan (4,698 devices)
Phase 2: Claude reasoning (no tool)
Phase 3: spawn_actor + set_actor_transform via MCP
Phase 4: verse_gen_* + verse_write_file (6,187 bytes, VerseBuild SUCCESS proven)
Phase 5: verse_patch_errors → Claude fixes → verse_write_file → [click Build] → repeat
Phase 6: world_state_export + snapshot_save
ONE HUMAN ACTION: clicking Build Verse (once per iteration)
NEXT (waiting for Epic):
system_build_verse ← expose BuildVerseCode to Python → Phase 5 becomes headless
FUTURE (zero human clicks):
device_catalog_scan → Claude designs the game from scratch
world_state_export → Claude reads the current level
spawn_actor (MCP) → Claude places all needed devices
verse_write_file → Claude deploys the code
system_build_verse → Claude triggers the compiler
verse_patch_errors → Claude reads errors + fixes + redeploys
LOOP → until VerseBuild: SUCCESS
snapshot_save → checkpoint
DONE → playable Fortnite Creative game, no human in the loop
The full pipeline reference: docs/PIPELINE.md
UEFN Toolbelt — Built by Ocean Bennett · 2026 · AGPL-3.0