Function definitions
Description: Declaring the functions your game provides so conditions and instructions can call them.

A lot of what a story needs to ask about, or do, lives in your game rather than in Narratyr — whether the player is carrying a sword, how much reputation they have with a faction, playing a cutscene, granting an item. A function definition is how you make one of those available to the expression language.
A definition is a declaration only, in the sense a C header file is: a name, the parameters it takes, and what it returns. Narratyr never sees an implementation and doesn't want one. That's enough for the editor to offer the function in autocomplete, check the calls you write against it, and export those calls for your engine to answer — while the actual behavior stays in your codebase, where it belongs.
The practical consequence is worth stating plainly: Narratyr knows nothing about a function
beyond its signature and whatever description you write. It can't tell you what
hasReputationWith("Legion") will return, or whether calling it twice is safe. Anything the next
author needs to know has to be in the description.
What a definition holds
Functions live in the Functions view, listed in a tree on the left with the selected one's details on the right.
| Field | What it's for |
|---|---|
| Name | How it's called in an expression. A valid identifier, unique in the document |
| Return type | string, integer, number, boolean, or void |
| Description | Free text. This is the only explanation anyone gets — write it |
| Pure | Declares the function has no side effects. See below |
| Parameters | An ordered list, each with a name, type, optional default, and optional description |
A parameter takes the same types a variable can, including enumerations, entity references, and lists or sets. Parameters are reorderable by dragging, and the order is the call order.
Return types are deliberately narrower than parameter types: a function hands back a single scalar or nothing at all. There's no list-returning or enumeration-returning function.
Two of the parameter fields exist purely to help whoever writes the call. A description appears alongside the parameter in the editor, and a default value documents what your implementation does when the argument is left out. Neither is something Narratyr enforces or supplies at runtime.
Declaring a parameter as an entity reference does earn you something concrete, though: autocomplete then offers the matching entities by name at that argument position and inserts the right id, so you're not copying ids by hand.
Built-in and user-defined
The tree splits into two groups.
Built-in functions are implemented by the Narratyr runtimes themselves. length, count, and
indexOf; randomInteger and randomNumber; debugLog; the traversal counters and
questStatus / objectiveStatus. You can call them without arranging anything — every shipped
runtime already answers them. They're shown read-only, can't be deleted, and are refreshed against
the current catalog each time a document loads, so a project made a year ago picks up whatever has
been added since.
The exception is if you're targeting an engine Narratyr doesn't ship a runtime for. Then the built-ins are yours to implement too, alongside the evaluator — see The expression AST, which lists exactly what each one has to do.
User-defined functions are the ones you declare, and the ones your game has to implement. Everything on this page other than this paragraph is really about them.
Where the implementation lives
Your game supplies a function host — an object the Narratyr runtime calls into whenever an expression reaches a function it doesn't implement itself. You bind it once at startup.
How you write it depends on the engine, and in two cases the export does most of the work:
- Unity generates a
NarratyrFunctionHostclass with one method per user-defined function, correct parameter and return types, and a not-implemented default. Subclass it, override the methods you care about, and assign an instance to the runtime. - Unreal generates a
UNarratyrFunctionHostobject whose methods are Blueprint native events, so each function can be implemented in C++ or in Blueprint, whichever suits it. - Godot generates nothing — the runtime looks the function up by name on whatever object you've assigned as the host, so any object with matching method names works.
In all three cases, re-export after adding a function and the new method is there waiting to be implemented.
Because the generated host is derived from the declarations, renaming a function in Narratyr renames the method your game overrides. Treat a rename the way you'd treat renaming an interface method — it's a coordinated change, not a cosmetic one.
If an expression calls a function and there's no host, or the host has no matching method, that's an evaluation error at runtime. Narratyr can't warn you about it beforehand, because the binding doesn't exist until your game runs.
Purity
A function can be marked pure, which declares that calling it changes nothing — it only reads
state and returns a value. hasInventoryItem is pure; addInventoryItem is not.
This is an honor contract. Nothing verifies it, and nothing can: the implementation isn't something Narratyr can see. Marking a function pure is a promise you're making to your own team and to future tooling, so mark it only when you mean it.
Purity is about side effects, not about being predictable. randomInteger and randomNumber are
deliberately not pure, even though they change no game state, because they carry internal
state of their own — calling one twice gives two different answers. Anything re-evaluated
repeatedly would behave erratically with one in it.
Not yet enforced. The flag is saved with your document, but nothing acts on it today. In particular, an Event Listener condition currently rejects every function call, pure or not, because the listener only re-checks when a variable changes and so can't notice a function's answer changing. Mark functions pure now and the distinction will be there when it's used.
Testing without an implementation
The simulator has no game attached to it, so it can't call your functions. Instead it mocks them, the way a unit test would.
When a simulation starts, every function call in the graphs is discovered and given an entry in
the mock panel: one per distinct set of arguments — hasInventoryItem("Sword") and
hasInventoryItem("Shield") are separate rows — plus a fallback for the bare function name that
catches any arguments not covered by a specific row. Set what each should return and the
conditions using them evaluate against it.
Mocks belong to the simulation session. They aren't saved with the document, and resetting the simulator clears them, which makes them a way to play out a scenario rather than a fixture to maintain.
Things worth knowing
- Argument types are checked; argument count is not. The editor flags passing text where a number is declared, but nothing flags passing two arguments to a three-parameter function — and the generated hosts don't rescue you either. On Unity and Unreal a missing argument arrives as zero or an empty string, so the call succeeds and quietly does the wrong thing; on Godot it fails at runtime. Count your arguments.
- Descriptions are the documentation. They appear as a tooltip in the tree, and on Unreal
they carry through into the generated host as a doc comment. A signature alone rarely says
whether
giveItem("Potion", 0)is a no-op or a bug. - Deleting a definition doesn't delete the calls. Expressions calling it become references to an unknown name, which the validator reports. Use Find References from the function's context menu before removing one.
- A Gate waiting on a function call needs a nudge from your game. Calls are allowed in a Gate
condition, but a parked Gate only re-checks itself when a variable is written or when quest or
objective progress changes. If the answer turns on something your game tracks — inventory, say —
call the runtime's
ReevaluateGateafter the change, or the Gate waits forever. Mirroring the value into a variable avoids the problem entirely. - An Event Listener can't call functions at all. It has no equivalent hook — it re-evaluates only on variable assignment — so the validator rejects every call in a listener condition, including pure ones. Mirror the value into a variable there.
Next
Expressions covers the language these functions are called from, Variables covers the other half of the shared state, and Exporting covers getting the generated host into your engine.