The IDL
You never write an IDL file. You write ASK, and its body is the IDL. This page is what that body
can say, what it turns into, and why it needs to exist at all.
The IDL is the price of ASK being one line.
Why it exists at all
A mod author never sees an IDL. A pack author never opens one either. The only place it surfaces
is inside ASK, so the real question is why asking your own question needs a declaration rather
than just being a function you write.
Because of what has to come out the other end. The moment a pack asks a question, that question has to be callable from a mod, and mods are written in six languages. Somebody has to produce all of this:
Without a declaration to generate from, a pack author writes every row above
by hand, per language. Nobody does that, so nobody would use ASK,
and the story about a pack exposing what makes its game different quietly
dies.
There is a second reason that is less obvious and more load-bearing. Tier W enforcement is the import table: a denied question is enforced by simply not appearing in the WASM imports, so the function does not exist for the mod to call. That only works if the import list and the host dispatch come from the same declaration. Hand-write either side and a mismatch becomes a trap with no useful error, and the sandbox guarantee gets softer than the docs claim.
Not a schema language you configure, and not something standing between you and the game. If it ever reads like a fourth noun to learn, that is a documentation failure rather than a design one.
Syntax
Two places it appears. A pack author writes the first; the second is how Axiom’s own built-in questions are defined, in the same language.
capability entity.query@1 { struct EntityId { index: u32, generation: u32 } bulk fn alive() -> [EntityId]; bulk fn within(centre: Vec3, radius: f32) -> [EntityId]; fn is_alive(id: EntityId) -> bool; fn count() -> u32; error StaleHandle; error PoolUnavailable; }
capabilityfnbulk fnstructerrorFailed code, generated into every binding’s error enumwritesWhy bulk is a keyword
Not a convention, not a naming rule. The generator emits a different marshaling path for it, and the linter warns when a scalar form is called inside a loop. It exists because the sandbox boundary is the only performance number that matters in a Tier W mod.
alive(), then transform(id) and world_to_screen(p) per entity, 400 entitiesalive(), transforms(ids), world_to_screen_many(ps), submit()What one definition generates
span and &[T] over guest memory, no copySpan<T>, no per-frame allocation// generated, stable within a major axiom_status axiom_entity_query_alive( axiom_ctx* ctx, axiom_span* out);
Every binding is a wrapper over that header, so a language with C FFI can target Axiom without waiting for official support.
Versioning rules
Changing what a function means while leaving its signature alone is a silent break. It is treated as a major even though nothing fails to compile, because the alternative is every mod on every game being quietly wrong at once.
Two majors coexist: entity.query@1 and @2 resolve to different dispatch entries, so two mods
in one session can use different ones and never see each other. A major is supported for two
subsequent majors or twelve months, whichever is longer, and axiom check warns at build time
with the replacement named.