Documentation menu

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() > 0 can 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 with 1, 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

  1. Add a variable in Variables, name it, and choose Expression from its type menu.
  2. Select the value cell, then select it again or press Enter to open it. Write one expression, using Ctrl+Space for completion. Enter inserts a newline inside the editor.
  3. Leave the cell or press Escape to commit the text and return to table navigation.
  4. 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:

  1. Add Highlight with type Expression and default false to a property set.
  2. Attach the set to an entity type with Usage set to choice.
  3. Select that type as Choice Type on a standalone Choice node, or on the dialogue node's type for choices embedded in dialogue nodes.
  4. Open one option's metadata editor and set Highlight to choiceTakenCount() == 0. Save it.
  5. In your game, read Highlight from 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) or notify_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.