Verse Wiki — the Verse handbook for Blueprint authors
Deep Dive · EXTRA

task and awaitable: What spawn Actually Returns

Blueprint's Set Timer by Event hands you a Timer Handle — hold onto it or you can never Clear it. Verse's spawn hands you a receipt too: the task object. This page nails down how to Await a task, why the task in Fortnite has no Cancel(), and the community's standard recipe for do-it-yourself cancellation.

1. task: Your Clone's Receipt

The main lesson says spawn "doesn't wait" — but it never said spawn walks away empty-handed. The spawn action pushes out a value: a task object. If Fn sends out something of type t when it's done, what you get is task(t) — a claim ticket reading "clone at work; redeem this receipt for the result". task implements the awaitable interface (think of it as an Event Dispatcher you Bind once and then wait for it to fire; the official API reference files it under /Verse.org/Concurrency), and its one core power is Await()<suspends>:t — stop and wait for this task to finish, then take the value it sends out.

potion_lab_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

potion_lab_device := class(creative_device):

    BrewPotion()<suspends>:int =
        Sleep(2.0)
        Print("Potion is ready")
        42

    OnBegin<override>()<suspends>:void =
        PotionTask := spawn{ BrewPotion() }
        Print("Clone is off brewing - main line stocks the shelves first")
        Sleep(3.0)
        Result := PotionTask.Await()
        Print("Pickup: a potion of potency {Result}")

Reading the graph: BrewPotion is a custom function that waits (Delay 2 seconds, Print String, finally send out the integer 42). In OnBegin, spawn forks off a wire to run it and hooks the task receipt into the variable PotionTask (:= = create a new variable and hook it up); the main line then Print Strings and Delays 3 seconds; then it calls Await() on PotionTask to collect the result into Result, and finally Print Strings it out.

The timeline: at second 0, the clone marches off (running in one go to its first waiting node, Sleep(2.0)) while the main line prints "stocks the shelves"; at second 2, the potion is ready, the clone clocks out, and the result 42 is stored on the receipt; at second 3, the main line wakes up and calls Await() — the task finished long ago, so Await returns 42 immediately, not one second wasted. That's the beauty of the receipt pattern: "start" and "collect" can be pulled apart, with any amount of other work slotted in between. You can also spawn a whole batch of tasks in one go, keep all the receipts, and Await them one by one later — "launch everything first, then wait together", the go-to move for parallel loading.

2. No Cancel(): Build Your Own Stop Event

On seeing a task, a seasoned developer immediately reaches for the familiar button: Cancel(). Bad news — the task interface exposed in the Fortnite environment has no Cancel() method, a long-standing community complaint. To call off a spawned clone mid-flight, the standard move is: open a race inside the spawned function — one wire does the real work, the other waits on a "stop event". When the outside wants to call it off, it gives that event a Signal (a shout); the event wire wins instantly, and the work wire is auto-cancelled at its next waiting node. You've borrowed race's cancellation rules to hand-fit the task with a brake.

patrol_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

patrol_device := class(creative_device):

    StopEvent:event() = event(){}

    PatrolLoop()<suspends>:void =
        race:
            block:
                loop:
                    Print("Patrolling...")
                    Sleep(1.0)
            StopEvent.Await()

    OnBegin<override>()<suspends>:void =
        spawn{ PatrolLoop() }
        Sleep(3.5)
        StopEvent.Signal()
        Print("Patrol called off")

Reading the graph: StopEvent is an Event Dispatcher with no payload (event()) — its only job is to shout "stop". Inside PatrolLoop sits a race: one wire is "loop: each lap Print String 'Patrolling...' then Delay 1 second", the other is StopEvent.Await() (Bound and waiting for it to fire). OnBegin spawns the patrol wire; the main line Delays 3.5 seconds, then gives StopEvent a Signal, and the patrol is called off.

Break it down: the loop wire has one Sleep(1.0) per lap — that's both the "yield and catch a breath" politeness (last lesson's rule) and the waiting node where cancellation can actually land; the StopEvent.Await() wire sleeps quietly until second 3.5, when the main line Signals it — it completes instantly and wins the race, and the patrol loop is promptly cancelled at its own Sleep. From the outside, the effect reads as "that spawned task got cancelled". One boundary note to close on: if the task should live and die with that stretch of graph anyway, just use branch — no need to build your own cancellation. The DIY brake is reserved for "must spawn, but want it recallable" situations.

What value does the spawn{ BrewPotion() } action push out?

3. Common Pitfalls: spawn vs. branch — Who Owns Your Clone's Life

Before you set out with your receipt, sidestep two collisions rookies keep having. Pitfall one: spawn's braces take exactly one call to a function "with the little clock" — no stuffing in a pile of nodes. Want "print, then Delay, then print"? Writing spawn{ a string of nodes } turns Compile red straight away. The fix is humble: pack those steps into a <suspends> function and spawn that. Pitfall two: don't expect a spawned wire to take its bow when that stretch of graph does. It escapes completely: the function that launched it has long since ended, and the clone is still running; when the round ends and the device is destroyed, the leftover wire goes poking at objects that no longer exist and slams into a runtime error. Which is exactly why the official guideline says: prefer structured concurrency (sync / race / rush / branch) over spawn.

Comparison spawn branch
Where it's allowed Works even on "wires that can't wait" (e.g. bound-event responses) Only on "wires that can wait"
What goes in the block One call to a function "with the little clock", nothing more Several nodes allowed
Task lifetime Escapes - keeps running on its own after the launching graph ends Revoked the moment the surrounding graph exits
Receipt Returns a task object you can Await() later No receipt - the nodes after it just continue

The one-line mnemonic: branch's clone is on a leash — when that stretch of graph wraps, it gets reeled in; spawn's clone has no leash, but you're holding a task receipt. Leash where a leash belongs, receipt where a receipt belongs — and as a bonus this explains a classic "haunting": put branch as the last step of a function and the task appears to "do nothing". In truth, the function ended immediately, the graph exited, and the clone was reeled in before it made it out the door.

This page is distilled from the official docs and community tutorials: Spawn in Verse ↗, awaitable — Verse API Reference ↗, and the community video What does spawn return? (UEFN Verse Boost Ep. 13) ↗.