Axiom/SDK
SDK / The IDL

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:

Generated from one ASK block
What it would cost by hand
C ABI entry point
Per question, matched by hand to the host
WASM import entry
Byte-compatible with host dispatch, or it traps
C++, Rust, C#, Java, Lua wrappers
Five bindings, per question, kept in sync forever
Bulk marshalling path
The array form, hand-written, per language
A reference page
Written separately, stale within a release
This is the whole argument

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.

What it is not

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.

IDLaxiom.idl
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;
}
Keyword
Meaning
capability
A question or primitive, with its major version
fn
One call, one crossing
bulk fn
Arrays in, arrays out, one crossing whatever the length
struct
A value type; length-prefixed across the boundary
error
A named Failed code, generated into every binding’s error enum
writes
Marks a call as a write, so it can be denied separately from reads

Why 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.

The same radar, written two ways
Crossings per frame
alive(), then transform(id) and world_to_screen(p) per entity, 400 entities
801
alive(), transforms(ids), world_to_screen_many(ps), submit()
4

What one definition generates

Output
Shape
Host dispatch
C++23, the core's side of the table
WASM imports
The import list. A denied question is absent from it, which is what makes Tier W enforcement real.
C ABI header
The actual contract, stable within a major
C++ / Rust
span and &[T] over guest memory, no copy
C#
Span<T>, no per-frame allocation
Java / Kotlin
Array views; also the native tier for JVM packs
Lua / JS
Tables. The only honest option, and a reason heavy per-frame work belongs in Tier W.
Reference docs
These pages
HEADERthe C ABI is the real contract
// 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

Change
Version move
New function added
Minor; old mods unaffected
Field added to a returned struct
Minor; structs are length-prefixed
Signature changed
Major
Field reordered or removed
Major
Semantics changed, same signature
Major
Units or handedness changed
Major, always
Required provider member added
Major
Optional provider member with a default
Minor; existing packs keep working
The row that is easy to get wrong

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.