Expressions
Description: Write graph logic and reusable formulas with Expression values.
Graphs use short expressions to make decisions and change state. The language supports common comparisons, assignments, and function calls without acting as a general scripting language.
- A condition evaluates to true or false. Gates, Hub outputs, choices, and Event Listeners
use conditions such as
Gold >= 10 && !HasMap. - An instruction changes state or calls a function. Instruction Nodes run statements such
as
JarlIsDead := true. - An Expression value stores a reusable formula in a variable or property field. Reading
the value retrieves its definition. Use
eval(Formula)or an engine evaluation API to run it.
Different from text directives. Player-facing text uses @{PlayerName} and
@{select ...}. These constructs follow the rules in Text directives.
Don't use @{...} in expressions or expression syntax in dialogue text.
JavaScript users. Many JavaScript expressions also work in Narratyr. Narratyr adds word
forms for logical operators, the in operator for list membership, and := for assignment.
Names and values
Expressions can refer to document variables and function definitions. The
editor reports unknown names while you type. Press Ctrl+Space in an expression field to see
the names available at the cursor.
Variable references must match the Variables view, such as Gold, Jarl_IsDead, or
Player_VisitedLocations, and can't contain dots. Use underscore characters for name prefixes.
You can write these values directly:
| Kind | Examples |
|---|---|
| Number | 42, -1.5, 0.25 |
| Text | "MacGuffin", 'a sword' with either quote style |
| True or false | true, false |
| Option set entry | QuestPhaseEnum.Active |
| List | ["a", "b", "c"] |
| Empty | null, undefined |
An option set entry uses a dot between the set name and entry key. It has the
form EnumName.EntryKey with no other parts. Expressions compare with the key instead of the
player-facing display name:
QuestPhase == QuestPhaseEnum.Active
Reference macros
Use reference macros to write readable names where an expression needs a stable ID. Arguments must be literal strings:
| Macro | Arguments |
|---|---|
#entity("TerribleTie") |
An entity's External ID |
#quest("The Heist") |
A quest's display name |
#objective("The Heist", "Find the Sword") |
A standalone quest's display name and an objective's display name within that quest |
#entity("TerribleTie") in Player_QuestInventoryItems
TargetObjective := #objective("The Heist", "Find the Sword")
Narratyr resolves macros at export and during simulation. The game receives the resolved ID. You can't pass a variable or a computed string as an argument. Names must match exactly, including case. Renaming a lookup key requires updating expressions that use it.
Entity and quest lookups search the whole project. Missing matches are errors. Multiple matches produce a warning and use the first match.
#objective requires both names and searches only the selected standalone quest, including
objectives in its called fragments. Missing or ambiguous quest or objective names are errors.
The lookup is independent of the graph containing the expression. It returns only the objective
ID, so objectiveStatus still needs its own quest argument.
For readable status checks, use the quest name and the objective's technical name directly:
objectiveStatus("The Heist", "Heist_FindTheSword") == "completed"
objectiveStatus accepts names without regard to case. Its objective argument accepts a
technical name or stable ID. To look up a display name, write
objectiveStatus("The Heist", #objective("The Heist", "Find the Sword")).
The longer names #externalIdToNarratyrId and #questIdFromName remain aliases for #entity
and #quest. The legacy #objectiveIdFromName("Find the Sword") retains its one-argument,
scoped lookup for existing projects. It starts with the current graph and searches fragment
callers before falling back to the whole project. It warns on multiple matches in the
selected scope and uses the first.
In stored Expression values, variable definitions and property defaults use project-wide macro lookup. An override written on a graph, node, or choice uses that graph's scope. A copied default keeps its original project-wide scope. Evaluating it from a different graph doesn't change its macro targets. Fix missing or ambiguous macro targets in stored Expressions before export.
Conditions
A condition can use a true or false variable by itself. JarlIsDead passes when the value is
true, and !JarlIsDead passes when the value is false. Numbers count as true when they aren't
zero.
Operators
| Operators | Behavior |
|---|---|
== != |
Test equality or inequality |
< > <= >= |
Compare numbers or text |
+ - * / % |
Perform arithmetic. + also joins text |
in not in |
Test membership in a list or set |
[ ] |
Read a list item by position, such as VisitedLocations[0] |
Combine conditions with and, or, and not. The symbols &&, ||, and ! provide the
same behavior. Choose one style for your project.
Parentheses group operations. Arithmetic runs before comparisons, comparisons run before
and, and and runs before or. Add parentheses when the intended order isn't clear.
Equality compares values with the same type. A comparison between text and a number returns
false. Check for an accidental quoted number when a condition such as Gold == "10" doesn't
pass.
Lists and sets
Use in or not in for membership. Built-in functions provide size, occurrence count, and
item position:
"MacGuffin" in Player_Inventory
ColorEnum.Red not in Banner_Colors
length(Player_Inventory) >= 3
count(Visited_Towns, "Riften") > 1
indexOf(Party_Members, "Sandra") != -1
length returns the number of items. count returns the number of matches, which is always
0 or 1 for a set. indexOf returns the zero-based position of the first match or -1 when
it finds none. Reading beyond a list with [ ] returns an empty value.
Visit counts
Built-in traversal functions let dialogue respond to earlier choices. Their counts cover the complete game and form part of saved runtime state.
choiceTakenCount()returns the number of earlier selections for the current choice. Use it in that choice's hide or disable condition. It can also appear in a stored Expression evaluated with a choice ID.choiceTakenCount() > 0can hide or disable an option after the player uses it.lastChoiceTakenCount()includes the choice that the player just selected. Conditions on outgoing connections can compare the result with1,2, or later counts to vary the response.traversed(id)returns the traversal count for a connection, node, or choice. Copy the required ID from the properties panel. The function can refer to elements in other graphs.
Use the choice-specific functions when possible because they don't require a stored ID.
An Expression that calls choiceTakenCount(), directly or through another Expression, needs
the current choice's context. Graph logic can call it only from a choice's hide or disable
condition. Game code can supply the choice ID when evaluating metadata. Without a current
choice, the runtime returns zero, but authoring checks still reject those graph uses.
Quest progress
questStatus("<quest id or name>") returns a quest status.
objectiveStatus("<quest id or name>", "<objective id or technical name>") returns an
objective status within that quest.
questStatus("IntroQuest") == "completed"
objectiveStatus("JarlQuest", "JarlQuest_FindTheSword") == "active"
Quest status values are "undiscovered", "inactive", "active", "failed", and
"completed". Objective status values are "locked", "active", "completed",
"failed", and "not_applicable". Both functions return "error" for an invalid reference
or unavailable quest data.
Completion lists quest IDs for questStatus. For objectiveStatus, it first lists quests and
then limits objective choices to the selected quest. It inserts an objective's technical name
when available and otherwise inserts its stable ID. Completion also lists valid status values.
Quest and objective name matching ignores letter case. Document checks report an unknown literal quest reference. They also warn when a quest name or objective technical name uses different capitalization from its definition.
A Gate can create an objective condition without typed syntax. Select the objective mode and choose objectives and statuses from the lists. The editor writes the equivalent expression.
Instructions
An Instruction Node contains statements separated by newlines or semicolon characters. Each statement changes a variable or calls a function.
Jarl_IsDead := true
Gold += 5
Persuasion_Attempts++
Player_Inventory += "OldMap"
addInventoryItem("Potion", 1)
| Form | Behavior |
|---|---|
Gold := 10 |
Assign a value. Gold = 10 has the same effect |
Gold += 5 |
Update with +=, -=, *=, or /= |
Gold++, Gold-- |
Add or subtract one |
MyList += "a" |
Append to a list or add to a set |
MyList -= "a" |
Remove the last list match or remove a set value |
addInventoryItem("Potion", 1) |
Call a function for its effect |
Compact updates state the operation directly. Gold += 5 and Gold := Gold + 5 produce the
same result.
For numbers, += and -= add or subtract. For lists and sets, they add or remove an item.
Adding an existing value to a set leaves the set unchanged.
Stored expression values
Choose Expression as a variable or property field's type when you want to store a formula
instead of its current result. The formula can return a number, text, true or false, an option
set entry, a list, null, or undefined.
A shop can use a variable named PriceFormula with this source:
BasePrice * 0.8
Here BasePrice is an existing numeric variable. An instruction can read the current price
with Price := eval(PriceFormula), where Price is another numeric variable. Changing
BasePrice changes the next result without editing the formula.
Author a definition
- Add a variable in Variables, name it, and choose Expression from its type menu.
- Select the value cell, then select it again or press Enter to open it. Write one
expression, using
Ctrl+Spacefor completion. Enter inserts a newline inside the editor. - Leave the cell or press Escape to commit the text and return to table navigation.
- Open the simulator and enter
eval(PriceFormula)in its REPL, using your variable's name. Change an input variable and evaluate again to check the new result.
For a field, choose Expression in a property set, write its default, and edit instance values on entities or graph metadata as needed. Metadata windows retain their Save and Cancel behavior. Edits outside a dialog commit when focus leaves the expression editor and can be undone.
Bodies can span multiple lines, but they must remain one expression. Assignments such as
Gold := 10, updates such as Gold++, and statement blocks belong in Instruction Nodes.
Invalid source remains editable and displays an error. Fix unknown references and syntax
errors before simulation or export.
The editor infers the result type from the body, including nested eval calls. It doesn't
ask you to choose a separate return type. Some expressions have more than one possible type:
DisplayName or 0 can return text or a number because or returns an operand value.
Narratyr's AI assistant and clients connected through the Model Context Protocol (MCP) can create Expression variables and edit values on existing entity, node, choice, and performance fields. Describe the formula and its inputs, or supply the source you want. The tools explain problems with the source and warn about impure calls. Create property schemas and edit graph-level metadata in the editor.
Evaluate explicitly
eval takes exactly one named Expression variable. A second Expression named
TaxedPrice could store eval(PriceFormula) * 1.1.
| Input | Result |
|---|---|
Price := eval(PriceFormula) |
Evaluates the definition and assigns its numeric result |
Price := PriceFormula |
Error: a definition isn't a number |
PriceFormula + 1 |
Error: operators can't consume a definition |
eval("BasePrice * 0.8") |
Error: eval doesn't accept source strings |
PriceFormula := 5 |
Error: runtime instructions can't replace a definition |
Game code evaluates fields through the engine methods linked below. A field read retrieves
the definition without evaluation. Conditions and dialogue text such as @{PriceFormula}
can't use an Expression directly. Store a computed result in an ordinary variable if text
needs to display it.
Lists and sets of Expression definitions aren't supported. Custom functions can't accept
Expression parameters or return definitions. A definition can still return a list, such as
[BasePrice, BasePrice * 0.8].
Defaults and blank values
Each instance can override a property's default with its own source. Changing a default doesn't replace an instance's stored definition. A missing value uses the normal property default rules. An explicitly empty or whitespace-only body evaluates to false. Clearing an override's text doesn't restore the property default.
Changing a variable or field to or from Expression resets incompatible values to the new type's default without evaluating the old definition. Undo restores the type change and its value resets together.
Choice metadata example
A game can use an Expression field named Highlight to decide how to draw a choice:
- Add
Highlightwith type Expression and defaultfalseto a property set. - Attach the set to an entity type with Usage set to
choice. - Select that type as Choice Type on a standalone Choice node, or on the dialogue node's type for choices embedded in dialogue nodes.
- Open one option's metadata editor and set
HighlighttochoiceTakenCount() == 0. Save it. - In your game, read
Highlightfrom the available path and evaluate it with that path's choice slot ID. Use the successful true or false result to style that option.
A choice the player hasn't taken evaluates to true. After the player takes it, that choice evaluates to false. Another choice with no selections still evaluates to true with its own ID.
Metadata stays passive: this field doesn't hide, disable, or select the choice. Use the existing hide and disable conditions when you want those behaviors.
Repeated evaluation and purity
Evaluation runs the body again using current variables, game functions, and quest status.
Definitions don't capture state or an implicit entity. The caller supplies the choice
identity and runtime services, and nested eval calls share them.
Definitions can call impure functions. The editor marks calls with a yellow warning
because repeated evaluation can change the result or cause side effects.
randomInteger(0, 10) can return a different value each time. A nested eval receives
the same warning when its definition calls an impure function.
Gates, objective completion gates, and Event Listeners require purity through every nested definition. Wrapping an impure call in an Expression doesn't make it valid there. Their variable and quest dependencies also include nested definitions, so input changes still refresh reactive conditions. See Function purity for game-owned state.
If a UI should make a random decision or cause a side effect once per interaction, evaluate once and store the result for that interaction.
Definitions are content, so runtime variable setters can't replace them and save snapshots don't contain them. Loading a save restores mutable inputs and leaves the current exported definitions unchanged. This also protects a definition when an older save contains a primitive value for a variable that has since changed to Expression.
Engine methods
Each engine guide shows named-variable and choice-metadata evaluation, result checking, and context construction:
Call game functions
Use a function for behavior that your game provides, such as an inventory query or an item grant. Define its name, parameters, and return type in the Functions view. Your engine integration supplies the implementation. See Function definitions.
hasInventoryItem("MacGuffin") && Gold >= 10
removeInventoryItem("OldMap", 1)
A function with a return value can appear within a condition. A function that returns void
runs as a statement in an Instruction Node.
Narratyr provides built-ins for lists, traversal counts, quest status, random values, and log
messages, plus eval for stored definitions. debugLog(message) writes a message to the engine log.
Expression rules
- Gates and Event Listeners require pure functions. Declare queries as pure in the
Function editor. After changing game-owned state, call
NotifyExternalStateChanged()(Unreal/Unity) ornotify_external_state_changed()(Godot) to refresh their conditions. - Expressions don't support comments. Use the node description or a Comment node to explain complex logic.
- The editor reports syntax and type problems. It marks unknown names, unmatched parentheses, and incompatible comparisons. Project checks report the same problems before export.
Custom engine support
The Unreal Engine, Godot, and Unity plugins include expression interpreters. A custom engine can use the exported abstract syntax tree instead. The expression abstract syntax tree documents the parsed format and evaluation rules.
Next
Function definitions explains callable game behavior, and Variables covers expression state. Dialogues and Quests describe graph nodes that use expressions. Text directives covers the separate syntax for player-facing text.