Reference Verse compiles

block Expression: Scoped Logic Inside Verse Code

In Verse, every code block must follow an identifier — you can't just open a new indented block in the middle of a function without one. The `block` expression is the solution: it lets you introduce a fresh nested scope anywhere inside your logic, constraining variable lifetimes and grouping related steps cleanly. Master `block` and your game logic becomes easier to read, reason about, and debug.

Updated Examples verified on the live UEFN compiler
Watch the Knotblock_expression in ~90 seconds.

Overview

Verse is strict about structure: a code block (indented body) must always follow an identifier like if, for, loop, a function name, or a class definition. When you need to group a sequence of expressions mid-function — perhaps to limit a variable's lifetime, clarify a logical phase, or simply keep related steps together — you reach for the block expression.

A block expression:

  • Introduces a new nested scope. Variables declared inside it cannot be referenced after the closing block.
  • Returns a value — the result of its last expression (just like a regular code block).
  • Can appear anywhere an expression is valid, including inside if branches, event handlers, or OnBegin.
  • Has no runtime cost — it is purely a scoping/readability construct.

When to reach for it:

  • You want to reuse a short variable name (like Result or Temp) in two separate phases without collision.
  • You want to visually separate "setup", "action", and "teardown" phases inside a single function.
  • You need the compiler to enforce that a temporary value isn't accidentally used later.

A block is NOT a loop, NOT a conditional, and NOT a coroutine boundary. It simply groups expressions and scopes their variables.

API Reference

(API surface could not be resolved for this device.)

Walkthrough

Scenario: A box-fight arena manager. When the round starts, two outer barriers go up and a mid-lane barrier goes down. A countdown timer runs. When time expires (SuccessEvent fires), the mid barrier re-enables and the outer ones drop — locking players in their lanes for the final showdown. Each phase is wrapped in a block so its local variables stay scoped to that phase.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

box_fight_manager := class<concrete>(creative_device):

    # Wire these up in the UEFN editor
    @editable
    Barrier1 : barrier_device = barrier_device{}

    @editable
    Barrier2 : barrier_device = barrier_device{}

    @editable
    MidBarrier : barrier_device = barrier_device{}

    @editable
    RoundTimer : timer_device = timer_device{}

    # Called when the timer completes successfully (countdown hits zero)
    OnRoundEnd(MaybeAgent : ?agent) : void =
        # --- Phase 1: lock outer lanes ---
        block:
            # These variables only exist inside this block
            OuterA := Barrier1
            OuterB := Barrier2
            OuterA.Enable()
            OuterB.Enable()
        # OuterA and OuterB are gone here — no accidental reuse

        # --- Phase 2: open mid-lane ---
        block:
            Mid := MidBarrier
            Mid.Disable()
        # Mid is gone here

    OnBegin<override>()<suspends> : void =
        # --- Setup phase: open outer lanes, seal mid ---
        block:
            Barrier1.Disable()
            Barrier2.Disable()
            MidBarrier.Enable()

        # Subscribe to the timer's success event
        RoundTimer.SuccessEvent.Subscribe(OnRoundEnd)

        # Start the round timer (no specific agent — global timer)
        RoundTimer.Enable()

Line-by-line explanation:

Lines What's happening
@editable fields Three barrier_devices and a timer_device wired in the editor.
OnRoundEnd handler Subscribed to SuccessEvent; receives ?agent (may be none).
First block: in OnRoundEnd Scopes OuterA/OuterB aliases — enables both outer barriers. After the block closes, those names are gone.
Second block: in OnRoundEnd Scopes Mid — disables the mid barrier. Clean separation of intent.
block: in OnBegin Groups the three setup calls into a named phase visually, even though no new variables are declared — purely for readability.
RoundTimer.SuccessEvent.Subscribe(OnRoundEnd) Wires the timer's completion event to our handler.
RoundTimer.Enable() Starts the timer device.

Common patterns

Pattern 1 — Scoped temporary variable that must not leak

You want to compute a "phase label" string and use it in exactly one place. Wrapping it in a block guarantees it can't be accidentally referenced later.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

scoped_var_demo := class<concrete>(creative_device):

    @editable
    Barrier1 : barrier_device = barrier_device{}

    @editable
    Barrier2 : barrier_device = barrier_device{}

    OnBegin<override>()<suspends> : void =
        # Phase A — enable both barriers, track count in a scoped var
        block:
            EnabledCount : int = 2
            Barrier1.Enable()
            Barrier2.Enable()
            # EnabledCount is valid here
            if (EnabledCount > 1):
                MidBarrier.Disable()
        # EnabledCount does NOT exist here — compiler enforces it

        # Phase B — later logic is clearly separate
        block:
            Barrier1.Disable()
            Barrier2.Disable()

    @editable
    MidBarrier : barrier_device = barrier_device{}

Pattern 2 — Using block as an expression that returns a value

block evaluates to the result of its last expression. You can assign that result to a variable in the outer scope.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

block_as_expression_demo := class<concrete>(creative_device):

    @editable
    MainBarrier : barrier_device = barrier_device{}

    @editable
    RoundTimer : timer_device = timer_device{}

    # Returns true if setup succeeded (last expression in block)
    SetupArena() : void =
        # The block groups setup; its last call is Enable()
        block:
            MainBarrier.Disable()   # open the arena
            RoundTimer.Enable()     # start the clock
            # last expression — block "returns" void here, matching SetupArena's return

    OnBegin<override>()<suspends> : void =
        SetupArena()

Pattern 3 — Ignore-list management scoped per phase

Add a VIP agent to the barrier's ignore list during a setup window, then remove all ignored agents when the round locks. Each operation is wrapped in its own block to make the intent crystal-clear.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

vip_barrier_demo := class<concrete>(creative_device):

    @editable
    GateBarrier : barrier_device = barrier_device{}

    @editable
    RoundTimer : timer_device = timer_device{}

    # Call this when a VIP player needs to pass through
    GrantVIPAccess(VIPAgent : agent) : void =
        block:
            # Only this block cares about VIPAgent in the ignore list
            GateBarrier.AddToIgnoreList(VIPAgent)
            # VIPAgent reference is still valid outside, but the
            # intent is clearly scoped to this operation

    OnRoundLock(MaybeAgent : ?agent) : void =
        block:
            # Lock-down phase: clear ALL ignore exceptions
            GateBarrier.RemoveAllFromIgnoreList()
            GateBarrier.Enable()

    OnBegin<override>()<suspends> : void =
        RoundTimer.SuccessEvent.Subscribe(OnRoundLock)
        RoundTimer.Enable()

Gotchas

1. block is NOT optional syntax sugar — it's required when you need a nested scope mid-expression

You cannot just open an indented block without a preceding identifier. If you try:

# WRONG — Verse won't accept a bare indented block
OnBegin<override>()<suspends> : void =
    Barrier1.Enable()
        Barrier2.Enable()   # syntax error — unexpected indent

Use block: to introduce that nested body legitimately.

2. Variables declared inside block are invisible outside it

This is a feature, not a bug — but it surprises newcomers:

block:
    X : int = 5
# X is unknown here — compiler error if you reference it

If you need a value after the block, declare it before the block, or capture the block's return value.

3. block returns the value of its LAST expression

If your last expression is void (like Enable()), the block is void. If you want to return a computed value, make sure the last line is the expression you care about — not a side-effecting call.

4. block does NOT create a new coroutine or suspension point

A block inside a <suspends> function inherits that context, but the block keyword itself does not add any concurrency. Don't confuse it with spawn or race.

5. Indentation must be consistent

Verse uses indentation to determine block membership. All expressions inside a block: must be indented exactly one level deeper than the block keyword itself. Mixed tabs/spaces will cause cryptic parse errors.

6. ?agent events must be unwrapped before use

When subscribing to events like timer_device.SuccessEvent (which sends ?agent), your handler receives MaybeAgent : ?agent. Unwrap it before calling agent-specific APIs:

OnRoundEnd(MaybeAgent : ?agent) : void =
    if (A := MaybeAgent?):
        block:
            # A is a real agent here
            GateBarrier.AddToIgnoreList(A)

Build your own lesson with block_expression

Generate a personalized, step-by-step lesson plan built around this object — grounded in this exact reference and our compile-verified knowledge base.

Build a lesson →