Overview
The spawn expression starts an independent async task. Unlike sync, race, or branch — which are structured concurrency expressions that must live inside an async (<suspends>) context and tie task lifetimes to a parent scope — spawn can be called anywhere: inside OnBegin, inside an event handler, even inside a non-suspending helper function. The spawned task becomes a free agent; it keeps running even after the scope that created it exits.
When to reach for spawn:
- You want a looping background task (e.g. a tide timer that runs forever) without blocking your
OnBegin. - You need to kick off per-player async logic inside a synchronous event handler (event handlers cannot themselves
<suspends>). - You want to fire a cinematic or teleport sequence for one player while the rest of the game loop continues uninterrupted.
The rule: the function you pass to spawn must carry the <suspends> effect. You cannot spawn a <decides> function.
This article also covers two devices that shine in spawn-driven designs:
player_spawner_device— places a player at a specific island location (SpawnPlayer,SpawnedEvent).teleporter_device— instantly moves a player to another location (Activate,Teleport,EnterEvent,TeleportedEvent).
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Scenario: The Sunny Dock Arrival
Your cel-shaded island has a wooden dock jutting into a sparkling cove. When the round starts:
- Every player is spawned at the dock via a
player_spawner_device. - A cinematic plays the boat-arrival sequence.
- A background task starts a 60-second tide timer using a
timer_device. - When a player steps into the teleporter at the end of the dock, they are sent to the clifftop — and a per-player async task logs the teleport without blocking the main loop.
Each of those things happens concurrently thanks to spawn.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# dock_arrival_manager — place this creative_device on your island,
# then wire up the @editable fields in the Details panel.
dock_arrival_manager := class(creative_device):
# The spawn pad sitting on the dock boards
@editable DockSpawner : player_spawner_device = player_spawner_device{}
# The teleporter at the far end of the dock (destination: clifftop)
@editable DockTeleporter : teleporter_device = teleporter_device{}
# Cinematic sequence: boat gliding into the cove
@editable ArrivalCinematic : cinematic_sequence_device = cinematic_sequence_device{}
# A timer device configured for 60 s countdown in the Details panel
@editable TideTimer : timer_device = timer_device{}
# ----------------------------------------------------------------
# Entry point
# ----------------------------------------------------------------
OnBegin<override>()<suspends>:void =
# 1. Subscribe to the dock teleporter so we react when anyone steps in
DockTeleporter.EnterEvent.Subscribe(OnPlayerEntersTeleporter)
# 2. Subscribe to the spawner so we know when each player lands on the dock
DockSpawner.SpawnedEvent.Subscribe(OnPlayerSpawned)
# 3. Play the boat-arrival cinematic for everyone (non-blocking — we spawn it)
spawn { PlayArrivalCinematic() }
# 4. Start the tide countdown in the background without blocking OnBegin
spawn { RunTideCountdown() }
# OnBegin can now return; both tasks keep running independently.
# ----------------------------------------------------------------
# Spawned task: plays the cinematic then stops cleanly
# ----------------------------------------------------------------
PlayArrivalCinematic()<suspends>:void =
ArrivalCinematic.Play()
# Wait until the sequence device signals it has stopped
ArrivalCinematic.StoppedEvent.Await()
# Cinematic finished — nothing else to do, task ends naturally
# ----------------------------------------------------------------
# Spawned task: starts the tide timer and waits for success/failure
# ----------------------------------------------------------------
RunTideCountdown()<suspends>:void =
TideTimer.Start() # kick off the 60-second countdown
# Await whichever fires first
race:
block:
TideTimer.SuccessEvent.Await()
# Tide came in — could trigger a flood hazard here
block:
TideTimer.FailureEvent.Await()
# Time ran out without success
# ----------------------------------------------------------------
# Synchronous event handler — called when a player spawns on the dock
# (event handlers cannot suspend, so we spawn async work from here)
# ----------------------------------------------------------------
OnPlayerSpawned(Agent : agent):void =
if (P := player[Agent]):
# Spawn a per-player async task without blocking the handler
spawn { WelcomePlayer(P) }
# ----------------------------------------------------------------
# Per-player async welcome — runs independently for each arrival
# ----------------------------------------------------------------
WelcomePlayer(P : player)<suspends>:void =
# Re-spawn the player firmly at the dock position
DockSpawner.SpawnPlayer(P)
# Small pause so the cinematic has a beat before gameplay starts
Sleep(2.0)
# Enable the teleporter for this session
DockTeleporter.Enable()
# ----------------------------------------------------------------
# Synchronous event handler — called when a player enters the teleporter
# ----------------------------------------------------------------
OnPlayerEntersTeleporter(Agent : agent):void =
# Spawn a per-player async journey task
spawn { SendPlayerToCliftop(Agent) }
# ----------------------------------------------------------------
# Async journey: teleport the player and wait for confirmation
# ----------------------------------------------------------------
SendPlayerToCliftop(Agent : agent)<suspends>:void =
# Activate sends the agent through the teleporter to its linked destination
DockTeleporter.Activate(Agent)
# Await the TeleportedEvent to confirm the journey completed
TeleportedAgent := DockTeleporter.TeleportedEvent.Await()
# TeleportedEvent sends the agent that emerged — task ends here
Line-by-line highlights
| Line | What it teaches |
|---|---|
spawn { PlayArrivalCinematic() } |
Launches the cinematic task without blocking OnBegin. OnBegin continues to the next line immediately. |
spawn { RunTideCountdown() } |
Starts a forever-running background task. Because it is spawned, it outlives OnBegin's own execution. |
OnPlayerSpawned(Agent : agent):void = |
Event handlers are synchronous (no <suspends>). You cannot Sleep or Await here directly — but you can call spawn. |
spawn { WelcomePlayer(P) } |
Creates an independent task per player. Each player gets their own concurrent welcome sequence. |
DockTeleporter.Activate(Agent) |
Sends the agent through the teleporter using the real teleporter_device.Activate API. |
DockTeleporter.TeleportedEvent.Await() |
Suspends the task until the teleport completes, then the task exits cleanly. |
Common patterns
Pattern 1 — Respawn a player at a named spawn pad
Use player_spawner_device.SpawnPlayer to programmatically place a player at a specific dock spawn pad (e.g. after they fall into the cove).
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
dock_respawn_manager := class(creative_device):
@editable CoveSpawner : player_spawner_device = player_spawner_device{}
@editable DamageZone : damage_volume_device = damage_volume_device{}
OnBegin<override>()<suspends>:void =
# When a player enters the cove's damage volume, respawn them at the dock
DamageZone.AgentEntersEvent.Subscribe(OnAgentEntersCove)
OnAgentEntersCove(Agent : agent):void =
if (P := player[Agent]):
# SpawnPlayer places the player at the CoveSpawner's location
CoveSpawner.SpawnPlayer(P)
Pattern 2 — Enable/disable a teleporter on a timer
Use teleporter_device.Enable and teleporter_device.Disable to open the clifftop teleporter only during a time window, using Sleep inside a spawned task.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
clifftop_gate_manager := class(creative_device):
@editable CliftopTeleporter : teleporter_device = teleporter_device{}
OnBegin<override>()<suspends>:void =
# Start the gating loop as a fire-and-forget background task
spawn { GateTeleporterLoop() }
# Cycles the clifftop teleporter: open 20 s, closed 10 s, forever
GateTeleporterLoop()<suspends>:void =
loop:
CliftopTeleporter.Enable()
Sleep(20.0) # open window
CliftopTeleporter.Disable()
Sleep(10.0) # closed window
Pattern 3 — React to SpawnedEvent and start a per-player async task
Subscribe to player_spawner_device.SpawnedEvent and use spawn to kick off a per-player async sequence from the synchronous handler.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
shore_greeter := class(creative_device):
@editable ShoreSpawner : player_spawner_device = player_spawner_device{}
@editable ShoreTeleporter: teleporter_device = teleporter_device{}
@editable ArrivalSeq : cinematic_sequence_device = cinematic_sequence_device{}
OnBegin<override>()<suspends>:void =
ShoreSpawner.SpawnedEvent.Subscribe(OnAgentSpawnedAtShore)
# Synchronous handler — must NOT suspend
OnAgentSpawnedAtShore(Agent : agent):void =
# Spawn an independent async greeting task for this specific agent
spawn { GreetAgent(Agent) }
# Async per-agent greeting: play cinematic, then open the teleporter
GreetAgent(Agent : agent)<suspends>:void =
ArrivalSeq.Play(Agent) # play cinematic for this agent
ArrivalSeq.StoppedEvent.Await() # wait until it finishes
ShoreTeleporter.Enable() # now let them through
Sleep(0.5)
ShoreTeleporter.Activate(Agent) # send them on their way
Gotchas
1. spawn is fire-and-forget — there is no automatic cleanup
The spawned task keeps running even if the device that created it is garbage-collected or the round ends unexpectedly. If your task loops forever (loop:), make sure it has a natural exit condition or it will run until the session ends. Use race with a sentinel event if you need cancellation.
2. Event handlers are synchronous — you cannot Await or Sleep in them directly
If you try to call Sleep(5.0) inside OnPlayerSpawned(Agent:agent):void, the compiler will reject it because the function lacks <suspends>. The fix is always: spawn { MyAsyncWork(Agent) } from inside the handler.
3. The function passed to spawn must have <suspends>
# WRONG — DoThing has no <suspends> effect
DoThing():void = Print("hello")
spawn { DoThing() } # compile error
# RIGHT
DoThingAsync()<suspends>:void = Sleep(1.0)
spawn { DoThingAsync() } # fine
4. You cannot spawn a <decides> function
Failable (<decides>) functions cannot be spawned. If you need to run a failable check asynchronously, wrap it in a <suspends> function that uses if to call the failable expression.
5. player_spawner_device.SpawnPlayer respects the device's ShouldRespawnAlivePlayers setting
If ShouldRespawnAlivePlayers is false in the Details panel and the player is still alive, calling SpawnPlayer will fall back to another valid spawn or skydive. Always configure the device in the editor to match your intent.
6. teleporter_device.Activate(Agent) vs Teleport(Agent)
Activate sends the agent to the teleporter's linked destination group (respects the device's channel/group settings). Teleport sends the agent directly to this device's location. Use Activate for normal portal gameplay; use Teleport when you want to pull a player to a specific teleporter pad regardless of linking.
7. listenable(agent) events send a plain agent, not ?agent
SpawnedEvent and EnterEvent hand your handler a concrete agent. You only need if (A := Agent?) unwrapping for events typed listenable(?agent) (like timer_device.SuccessEvent). Mixing these up causes a type error.