Scripts on a node
Making something happen when the story arrives — where a script lives, a script on an item, what it may do and touch, and what a failure costs.
All documentation — Variables, logic and commands
Boards and the canvas
The node builder
Modelling the world
Variables, logic and commands
Voice and audio
AI
Working together
Project settings
On this page
- Where a script lives
- A script on an item
- self is the item the script is on
- Script receives — the items the game hands it
- Changing every item in a list
- When it runs, and in what order
- Everything a script may do
- What a script may touch
- Building one without typing
- A worked shape
- When a script fails
- The budget
- The one refusal that is not a coercion
- Visual or Code
A script runs when the story arrives at a node. It is where state changes: setting a variable, adding to an inventory, telling the game to play a cutscene.
If a condition asks may the story go this way?, a script answers and what happens now that it has?

The language itself is the variables reference.
Where a script lives
A script is the text of a field whose type is code, on a node. The standard pack’s Code node —
runs an expression when the flow reaches it — is a node type with one such field and nothing else,
but any node type you author can carry one, and can carry several.
That is worth knowing because it changes how you build: a Dialogue node with a code field runs its
script and says its line. You do not need a separate Code node beside every line of dialogue.
A script on an item
A code or condition field can also live on an entity definition, so every item of that kind
carries its own script — a card’s On use, a door’s Can open. Your game runs it when it
decides to (play mode in the browser runs node scripts only).
self is the item the script is on
Inside a script stored on an item, self means that item:
target.hp -= self.damage + user.strengthWrite self, never the item’s own code, and duplicating the item gives you a working script with
nothing to edit: Iron Dagger copied from Rusty Dagger hits for its own damage. Typing
self. lists the fields of the item’s kind.
self means something only there. In a node’s script it is underlined: ‘self’ only means something
in a script on an item. Here, name the item by its code instead. For the same reason nothing in the
project may be called self — not an item, a variable, a group, a role or a name inside a script.
Script receives — the items the game hands it
Most item scripts are about something else as well: the enemy a weapon hits, the hero using it. Name those in the field’s settings (the gear on the field) under Script receives: one row per name, and for each the kind of item it holds.
| Name | Kind of item |
|---|---|
target | Combatant |
user | Combatant |
From then on the script can write target.hp and user.strength. Typing target. lists a
Combatant’s fields, and a misspelling is underlined at once: ‘target’ (Combatant) has no field called
‘hpp’. A name the field does not receive is underlined too, with a pointer to Script receives.
A name there follows the same rules as any name a script makes up: letters, digits and underscores;
not a word the language keeps for itself; not self; used once — and Target or tar_get count
as target again, because a game engine reads them as one name; and not the code of an item or a
group — inside the script, the name would hide it. Unlike a temporary name, it may not be the code
of a variable, a media file or an option set either: the row lives in the field’s settings, not in
the script, so a role called gold would quietly turn every gold already written on that kind of
item into the role, and one called Mood every Mood.calm. Case does not tell them apart: Gold
counts as gold. It works the other way round too — once a role is called target, a new variable
or media file named “Target” gets the code target_1, and renaming one to target is refused
(a variable inside a group is read as group.target, so it may still use the name).
In a condition field on an item, the visual builder lists these too: This item for self’s
fields and Roles for the names it receives, so target.hp > 0 can be picked rather than typed.
When the game runs the script, each item it hands over must be of the kind the row names, or of a
kind made from it: a Boss made from Combatant can be the target, a Potion cannot. An item
of another kind, or a script on an item that calls a command the story waits for, is refused before
the script changes anything, and the game is told why.
Changing every item in a list
A loop over a field that holds a list of items reaches each item in turn, and can change it:
for (w in user.weapons) w.damage += 1endforThat is a whetstone sharpening every weapon its user carries. When the list field allows only one
kind of item (its Allowed Entity Definitions names exactly one), typing w. lists that kind’s
fields and a wrong field name is underlined. When it allows several kinds, w.anything is accepted
as written — the editor will not guess which kind it is.
When it runs, and in what order
At each node, in this order:
- Every script slot runs, in document order.
- The node presents a step, if it has content.
- Its exits are resolved — any exit with content means choose, otherwise the first passing gated exit is taken, otherwise a single exit advances, otherwise the story ends.
So a script has already run by the time the node’s own exits are gated. A variable you set in step 1 is visible to a gate in step 3, on the same node.
Everything a script may do
Six statements. There is nothing else.
| Statement | What it does |
|---|---|
| Assignment | gold = 50, gold += 10, kim.trust -= 1. Writes a variable or an entity field. |
| A method call | inventory.add("key"), flags.toggle("met_kim"), party.clear() |
| A command call | playCutscene("act2_open") — tells the game to do something |
let | let total = gold + silver — a block-scoped local |
for | for (item in inventory) … endfor, with break and continue |
if | if (…) / elseif (…) / else / endif |
What a script may touch
| It can | It cannot |
|---|---|
| Read and write variables | Create a variable, a tag or a node type |
| Read and write entity fields | Change a definition’s schema |
| Call a command | Reach the network or the file system |
| Read the visit count of a node | Read a wall clock |
| Ask whether an entity exists | Change the project |
| Draw from the playthrough’s seeded random | Use an unseeded random |
Random is seeded, always. The playthrough owns a generator, a draw advances it, and its state is
part of a save — which is what makes a save replay identically. That is also why random() is
refused in a condition.
Building one without typing
A node’s script opens in Edit Code:
Build code visually using statements. The generated code is shown in the preview panel. Changes save automatically.
Unlike a condition, this surface has no Visual/Code toggle — the right-hand column shows the generated code read-only. You build with statement rows.
Add a statement offers these, searchable by What should happen?:
| Statement | What the menu says |
|---|---|
| Set | Put a value into a variable — typed, another variable, or worked out. |
| Change | Add to, or take from, what a variable already holds. |
| Let | Give a value a name, just inside this block, so you only work it out once. |
| Call | Do something the game knows how to do. — only when the project has commands |
| If | Do one thing when a test passes, and something else when it does not. |
| For | Do the same thing once for every thing in a list. |
| Break | Stop repeating and carry on after the block. — only inside a For |
| Continue | Skip the rest of this turn and go straight to the next thing. — only inside a For |
| Note | A line for you, ignored when the story runs. |
An empty search also offers one ready-made pattern, Make a random check — Roll a die, remember
it, then branch on the result — which writes let roll = roll(20) and an if roll >= 10.
A statement the builder cannot draw is kept as a raw code row rather than being lost, with its parse error inline. When any row is unhappy the dialog says Some statements need attention - delete or fix them.
The side panel has two tabs: Generated code, and What you can write — a searchable vocabulary browser grouped as Your variables, Your entities, Your commands, Your option sets, Actions on a set, Numbers, Text, Lists, The story so far, Chance and Writing a script.
A worked shape
if (gold >= 200) gold -= 200 inventory.add("lantern") flags.add("bought_lantern") playSound("purchase")else playSound("denied")endifTwo things to notice. inventory.add(…) and flags.add(…) are methods on a variable, not
functions — every argument is flattened, so add(list) adds each member. And playSound(…) is a
command: it crosses into the game, and it is the only line here that a gate could not contain.
When a script fails
The errors themselves — what each code means and the exact wording — are on the variables reference.
&& and || absorb an error on one side when the other side already decides the answer.
The budget
| Limit | Value |
|---|---|
| Expression size | 5,000 parts |
| Operations per invocation | 200,000 |
| Nesting depth | 64 |
| Items in one value | 100,000 |
| Characters in one value | 1,000,000 |
Exceeding one is an error like any other: the step still happens, and you are told which limit and by how much.
The one refusal that is not a coercion
The language coerces almost everything — a list read as a number becomes its length, null read as
text becomes empty. One mismatch is refused outright: assigning a list into a variable that
cannot hold one.
'gold'holds a number, so it can’t be given a list. Take a single value out of it first —count(…)for how many there are, orfirst(…)for one of them.
The reason is where the damage would go. Every other mismatch produces a wrong value of the right shape, and it stays inside the playthrough. This one produces a wrong shape, and it travels into the export and into a typed field in your game.
for … in refuses a non-list for the same reason, rather than quietly looping once.
Visual or Code
A script has the same per-control toggle a condition does, and the same guarantee: text is the source of truth, and the rows are a projection of it, parsed by the same parser and serialised by the same encoder. Anything the builder cannot draw is preserved byte for byte, and an edit splices only the stretch of source it came from.
Code → Visual is refused while any control does not parse. Visual → Code always works.