NEOLearn

Scripting

How scripts work on NEO, where they live in a project, and what a script looks like in JavaScript and Python.

Updated October 17, 2026

On this page

Scripts add behaviour to a NEO game without rebuilding it: moving platforms, puzzles, timers, spawning objects, reacting to players. The game itself is built in Rust; scripts run on top of it and can only touch what the game chooses to expose.

What a script is#

Every script on NEO runs on one script VM. You can write scripts in JavaScript or Python (NEO's Python is called NeoPy). Both languages compile to the same engine, so they share the same rules for time, randomness, limits and permissions.

A few things are true of every script:

  • One language per game. A game's neo.toml picks js or py, and all of that game's scripts and modules use it.
  • Deterministic. The same scripts, inputs and world seed produce the same result on every run and every device. Time is simulation time, randomness comes from a world-seeded stream, and there is no wall clock.
  • Sandboxed. A script can't read files, open network connections, use eval or change built-in objects. One script can't reach another script's variables or state; scripts only meet through the world.
  • Nothing is reachable unless the game exposes it. A script only sees the entity kinds, fields and methods that the game declares in Rust and grants in neo.toml. Anything else doesn't exist for the script.

Where scripts live#

Script files go in your project's assets/scripts folder (.js or .py). The [script] section of neo.toml names the language, lists the scripts the game runs, sets their budgets and says what they may touch:

neo.toml
[script]
language = "js"
scripts = ["assets/scripts/platform.js"]

[script.expose.Platform]
fields = { position = "get set", speed = "get" }
methods = ["boost"]

A script's name is its file name without the extension, so assets/scripts/platform.js runs as platform. Files that share code with your scripts are modules: they live in the same folder and export functions instead of a world class.

The full list of keys is in The [script] manifest.

The shape of a script#

A script exports one class that extends World. Methods marked with @on(...) run when an event happens, and state holds the values the script keeps between ticks:

counter.js
import { World, on } from "neo";

export default class Counter extends World {
  state = { ticks: 0 };

  @on("tick")
  count(dt) {
    this.state.ticks += 1;
  }
}
  • World gives the script its view of the game: state, emit, query, spawn, rng and time. See The World API.
  • @on("tick") runs the method every simulation tick. Games send their own events too, such as "touch" or "interact". See Events and handlers.
  • state survives hot reloads and is part of the script's state hash. See Script state.

Running and editing scripts#

The game loads the scripts listed in neo.toml when it starts. In the Studio, the Scripts panel shows every running script with its fuel, faults and state hash, and saving a script hot-reloads it while the game runs. See The Scripts panel. The same controls are available as commands for tools and agents: see Script commands.

In this section#

Next steps#