Skip to content

Core patterns

How guido applications behave, in five parts. Code samples are Bun; Python is the same with snake_case names.

Writes queue, reads round-trip

Setting a property does not wait for the runtime. The SDK queues the write and returns; queued writes are sent in batches automatically. This is why building a screen with hundreds of set() calls is fast.

A get() asks the running engine and waits for the answer. When a read must observe writes you queued earlier, call sync() between them; it resolves after everything sent before it has been applied:

ts
label.set("text", "42");      // queued, returns immediately
await client.sync();           // barrier
await label.get("text");       // returns "42"

sync() is rarely needed: UIs mostly write, and the SDKs cache values you wrote locally, so reading back your own writes does not round-trip.

Signals

Widgets report events (pressed, text_changed, item_selected) as signals. Subscribe with a callback:

ts
const sub = await slider.on("value_changed", (v) => setVolume(v as number), {
  throttleMs: 50,
});
// later: await sub.off();

Options: once: true unsubscribes after the first emission; throttleMs rate-limits signals that fire every frame (sliders, scroll positions). Signal names are listed in the Godot documentation for each Control, or ask the runtime (see reflection below).

In Python, callbacks run on the SDK's reader thread. Calling SDK methods from a callback is safe; touching another framework's widgets (Tk, Qt, asyncio) is not. Marshal to that framework's thread first.

Ownership

A widget you create is yours until you addChild it into the tree; after that the tree owns it, and removing a parent frees its children. Free anything you created but never parented (obj.free()). The runtime prints a warning at exit for each leaked object, which is a usable leak check in CI.

Object handles are opaque tokens. Store them and pass them back; do not parse or compute with them.

Declarative layers

Create/set/subscribe is the wire model, not a required programming model. For larger UIs, use a declarative layer:

  • Scenes: design complete screens in the Godot editor and bind to nodes by name (Custom scenes & assets).
  • godot-vue: a Vue 3 renderer over the SDK, with components, reactivity and v-model rendering into widgets. The same approach can be implemented for other frameworks.

Either way, imperative code shrinks to event handling.

Runtime reflection

guido builds are trimmed, so the reliable way to know what a given runtime supports is to ask it:

ts
const hs = await client.connect();
hs.features;                                        // capability list
await client.request("classes", { contains: "Tree" });      // available classes
await client.request("classInfo", { className: "Button" }); // properties, signals, methods

classInfo output is complete enough to generate typed bindings from; the SDKs' generated classes are produced this way.

Teardown

disconnect() flushes queued writes and shuts the runtime down: spawned engines receive EOF and exit on their own; in-process engines pump two final frames so late writes are applied. Call disconnect() (or client.quit()) rather than killing the process, or the final batch of writes may be lost.