Every long-running call in the XBOX Godot Sample addons returns a one-shot
Godot Signal that resolves to a typed Result object. The
pattern is identical across godot_gdk, godot_playfab, and (when
relevant) godot_gameinput, so once you know it for one method you
know it for all of them.
This page is the one-page intro the tutorials assume. For the deeper
implementation view of the Microsoft GDK side specifically (native runtime
queue, XAsyncBlock bridge, XTaskQueueHandle), see
gdk/async-system.md.
A method whose name ends in _async returns a Godot
Signal
that fires exactly once when the underlying Microsoft GDK / PlayFab /
GameInput operation completes:
| Addon | Example |
|---|---|
godot_gdk |
GDK.users.add_default_user_async() |
godot_gdk |
GDK.achievements.update_achievement_async(user, id, %) |
godot_playfab |
PlayFab.users.sign_in_with_xuser_async(xbox_user) |
godot_playfab |
PlayFab.multiplayer.create_lobby_async(user, config) |
Methods without the _async suffix are synchronous — they
return the value directly (GDK.is_initialized() -> bool,
GDK.presence.get_cached_presence(xuid) -> GDKPresenceRecord).
You do not need connect() or callbacks for one-shot completions.
await works directly on the returned Signal:
func sign_in() -> void:
var result: GDKResult = await GDK.users.add_default_user_async()
if not result.ok:
push_warning("[Auth] silent sign-in failed: %s" % result.message)
return
print("[Auth] signed in as %s" % result.data.gamertag)The signal fires on the main thread during the addon's per-frame
dispatch tick, so your await resumes from a safe context — you
can touch scene-tree nodes, mutate Godot objects, or call further
_async methods directly.
If you want to drive several calls in parallel and wait for them together, use a small fan-in helper rather than awaiting each one serially:
func warm_caches() -> void:
var ach_signal: Signal = GDK.achievements.query_player_achievements_async(Auth.xbox_user)
var board_signal: Signal = PlayFab.leaderboards.get_leaderboard_async(
Auth.playfab_user, "high_score", 1, 25)
var ach_result: GDKResult = await ach_signal
var board_result: PlayFabResult = await board_signal
# Both calls were in flight at the same time; this function waits
# only as long as the slower of the two.Each call's signal is independent, so two awaited signals do not serialize each other.
Every async call resolves to a normalized result type that carries the success bit, a payload, and an error description:
| Addon | Result class | Success check | Payload field |
|---|---|---|---|
godot_gdk |
GDKResult |
result.ok |
result.data |
godot_playfab |
PlayFabResult |
result.ok |
result.data |
godot_gameinput (rare async methods) |
GameInputResult |
result.ok |
result.data |
A typical handler looks like:
func _push_progress(percent: int) -> void:
var result: GDKResult = await GDK.achievements.update_achievement_async(
Auth.xbox_user, "1", percent)
if not result.ok:
push_warning("[Ach] update failed: %s (%s)" % [result.message, result.code])
return
print("[Ach] Updated to %d%%" % percent)result.data is the typed return value for the operation —
GDKUser for sign-in, a Dictionary for a PlayFab leaderboard
fetch ({ rankings: Array, version: int, ... }),
PlayFabLobby for a lobby create / join, and so on. The doc_classes
XML page for the service describes the exact data shape per
method (press F1 on the service class name in the Godot editor
to read it).
When result.ok is false, result.message is a short
human-readable description and result.code is a stable string id
you can branch on:
match result.code:
"no_default_user":
# Expected on a clean PC — fall through to UI fallback.
return await _ui_fallback()
"title_id_required":
push_error("Set playfab/runtime/title_id in Project Settings.")
_:
push_warning("Unhandled: %s" % result.message)There are two distinct error surfaces. Tutorials lean on both:
-
The awaited
Result— your call's failure. This is what you get back from theawait. Use it to drive per-call recovery ("the silent sign-in failed, fall back to UI"). -
Service-level
runtime_errorsignals — failures that surface between your calls (network dropped mid-frame, a background fetch refresh failed, the Achievements Manager bubbled a service error during dispatch). Wire these once at startup to drive global UI state like "Achievements offline":func _ready() -> void: GDK.achievements.runtime_error.connect(_on_achievements_runtime_error) GDK.social.runtime_error.connect(_on_social_runtime_error) func _on_achievements_runtime_error(result: GDKResult) -> void: push_warning("[Ach] subsystem error: %s" % result.message)
Service-level runtime signals exist on most Microsoft GDK services that have
their own native callback path (GDK.achievements, GDK.social,
GDK.presence, GDK.multiplayer_activity, …) and on the major
PlayFab services that wrap a background callback queue
(PlayFab.multiplayer.multiplayer_error,
PlayFab.party.party_error). Press F1 on a service class in the
editor to see whether it exposes one.
Both addons pump async completions automatically each process
frame via the gdk/runtime/embed_dispatch and
playfab/runtime/embed_dispatch project settings (default true).
You only need to call GDK.dispatch() or PlayFab.dispatch()
yourself when:
- you turned
embed_dispatchoff for deterministic test control - you want a synchronous pump from outside the engine main loop (rare — usually only test scaffolding)
- you are on an older Godot (4.3 / 4.4) where
_processdoes not fire reliably from autoloads
In normal app code you should never need to call dispatch().
- Don't
awaitinside aforloop on a per-frame basis. Eachawaityields back to the engine; a per-frame await chains four frames of latency onto a four-element loop for no benefit. Build the requests in parallel (see "fan-in helper" above), or batch with the service's bulk method when one exists. - Don't drive sign-in from
GDK.users.user_changed. That signal fires for every user lifecycle event (adds, removes, picture changes, privilege updates). Use the dedicated_asyncentry point inAuth.sign_in()(see Tutorial 1) — it's idempotent and joins any in-flight attempt instead of starting a second one. - Don't shut the runtime down inside an
await. CallGDK.shutdown()/PlayFab.shutdown()from_exit_treeonly after every in-flightawaithas resolved. Outstanding signals fire on shutdown with a failure result; if youawaitsomething that races with shutdown your handler resumes in a partly torn-down state.
gdk/async-system.md— the deep view of the Microsoft GDK side (native runtime,XAsyncBlockbridge,XTaskQueueHandleownership, per-serviceruntime_errorsemantics).gdk/api-reference.md— the full Microsoft GDK surface, organized by service. Every_asyncmethod is listed with its return signal andResult.datashape.playfab/plugin.md— the PlayFab side, including which services exposeruntime_errorstyle signals.- Every tutorial under
tutorials/— every snippet in the tutorial chain follows the patterns described here.