Skip to content

Custom types & collections

Sometimes a single scalar—a health float, a name string—isn’t enough. A player’s inventory might be a list of items, or a buff system might track active effects keyed by name. Custom types let you define reusable composite shapes and then reference them in entity schemas as lists or dictionaries, or as flat fields in command schemas.

Create a .yaml file in the directory set by types_schema in golem.yaml (default: schemas/types/). Each file defines one named type:

schemas/types/item.yaml
type: Item
fields:
id: { tag: 1, type: int32 }
name: { tag: 2, type: string }
count: { tag: 3, type: int32 }
  • type: PascalCase name used to reference this type in entity schemas and command schemas.
  • fields: a map of snake_case field names. Each field requires an explicit type (proto scalar: int32, string, double, bool, etc.) and a tag.
  • tag: a required positive integer, unique within the type. It maps directly to the Protobuf field number and must stay stable — see Tags below.

Custom types are global — any entity schema or command schema can reference them.

Once you have custom types defined, use list<T> and dict<K,V> as the type value for any entity field:

schemas/entities/player.yaml
entity: Player
vars:
inventory: { tag: 1, type: list<Item>, sync: tick }
active_buffs: { tag: 2, type: "dict<string, Item>", sync: tick }
score: { tag: 3, type: int32, sync: tick }
  • list<T> — an ordered list of T. T can be any proto scalar (int32, string, etc.) or a custom type.
  • dict<K, V> — a map keyed by K (any proto scalar) to values of type V (proto scalar or custom type). Spaces around the comma are optional.

Run golem-bake after adding or editing custom types or collection fields.

You can also use a custom type directly as a field type on a command—useful when a command payload is too structured for individual scalars:

schemas/commands/use_item.yaml
command: UseItem
target: entity
entity_type: Player
fields:
item: Item

The same Item struct and interface that entity code uses are available in command handlers and on the JavaScript client—no extra types are generated. See Command schemas for a full walkthrough.

For each custom type, bake emits a Go struct and a TypeScript interface. The same definitions are used whether you reference the type in an entity schema or a command schema—no extra code is generated per usage site.

// Generated — do not edit
type Item struct {
Id int32
Name string
Count int32
}

Entity fields typed as list<Item> become []Item in the state struct and []Item (nil when unchanged) in the delta struct:

type SyncedPlayer struct { ... }
func (e *SyncedPlayer) Inventory() []Item { ... }
func (e *SyncedPlayer) SetInventory(v []Item) { ... }

Passing a non-nil empty slice ([]Item{}) to a setter clears the collection on the next delta. Passing nil has no effect (the field stays as-is).

Bake emits a TypeScript interface for each custom type and typed getters on the entity class:

export interface Item {
id: number;
name: string;
count: number;
}
// On SyncedPlayer:
get inventory(): Item[]
get activeBuffs(): Record<string, Item>

Collections use replace-whole semantics: when any element changes, the entire collection is sent in the delta, not individual element diffs. For each tick, each collection field is either:

  • Absent — nothing changed; the client keeps what it has.
  • Present (possibly empty) — the new authoritative value replaces the previous one entirely. An empty collection here means the field was explicitly cleared.

This makes reasoning straightforward: every delta you receive for a collection is the current complete value of that field.

Every field in a custom type must have a tag — a positive integer that maps directly to a Protobuf field number. Tags must be unique within their type:

fields:
id: { tag: 1, type: int32 } # required; must be unique within this type
name: { tag: 2, type: string }

You can number freely from 1, leave gaps, and add new fields at any unused tag. The only constraint is that you never reassign an existing tag to a different field — that would corrupt any stored data that uses this type. Missing or duplicate tags are a bake error.

  • Custom type names must be PascalCase and unique across all files in your configured types directory (default schemas/types/).
  • Every field must have a tag integer ≥ 1, unique within the type. Missing or duplicate tags are a bake error.
  • Field types in custom types must be proto scalars — nesting custom types inside other custom types is not supported.
  • list<T> and dict<K,V> fields are only valid for entity vars—not for command fields. Flat custom types (e.g. type: Item) are valid on command fields. For world schemas, collections are generated automatically by catalog sources—see World schemas.
  • If a collection field references a custom type that doesn’t exist in your types directory, golem-bake fails with an error.
  • Collection fields participate in sync like any other tick field: they are tracked per-tick and sent only when dirty.

After edits, run golem-bake to regenerate all outputs.