Talemark syntax reference

A story is a folder containing one story.talewright.yaml, optional supporting *.talewright.yaml files in subfolders, and any number of *.tale files. Talemark is the story-authoring language that powers Talewright stories, written in .tale files. It lets you mix prose with bold, italic and code formatting, dialogue, choices and story rules. The YAML manifest is a separate settings file for the story’s characters and world; it is not Talemark source. Talemark supports some Markdown-style formatting, but a .tale file is not a general Markdown document.

New to Talewright? Start with the author guide. The examples below are a quick start; the rest of this page is the full reference. You can use the examples as patterns and look up individual features when you need them.

Quick start: the shape of a story

A story uses two kinds of files:

  • story.talewright.yaml is the settings file (also called the manifest). It gives your story a title, tells Talewright which scene to start with, and names the player character.
  • A .tale file holds the scenes: the words, dialogue and choices the player sees.

For example, the manifest can start like this:

game:
  id: my-story
  title: My Story
  version: 0.1.0
  start: crossroads
characters:
  player:
    first-name: Alex

Create a .tale file beside the manifest, then write a scene like this:

=== crossroads
@title The Crossroads

Two paths leave the village.

* [Take the forest path] -> forest
* [Go home] -> END

=== forest
@title The Forest

The trees close behind you.

-> END

=== crossroads starts a scene (one part or place in your story); its name is the scene ID. @title is the heading players see. Write ordinary story text on its own lines. A line beginning with * makes a choice; the words in brackets are what players click, and -> says where that choice goes. -> END finishes the story.

To check or play your story without installing the desktop tools, zip its folder and open the zip in the web player. For the full walkthrough, see Your first story.

How to use this reference

The sections below cover all settings and Talemark features, including optional advanced features. If you are writing a straightforward story, you can start with the example above and the writing guide. You do not need hubs, custom C# rules, or every setting in this reference to write or share a story.

The whole story is checked when it loads. Mistakes are reported with a file name, line and column, plus an error code, so you can fix them before playing. C# story projects are an optional developer feature; see C# extensions.

Identifiers are names for scenes, characters, items and other story parts. Use letters, numbers, underscores, and single dots or hyphens between words; start with a letter or underscore. For example, temple.entrance and hidden-door are valid names. A dot groups words in a name; it does not mean “look inside” another value. Lowercase hyphenated words (kebab-case) are a style recommendation, not a parser restriction. The naming lint rules TWL001/TWL002 warn about style; they do not make an otherwise valid name illegal.

The Talemark operator registry is a deeper technical reference for symbols, calculation order and quoting rules. Most authors will not need it for a first story.


1. story.talewright.yaml

The root manifest owns game:. Supporting configuration files may declare characters, items, factions, flags, and numeric or text vars, using the same section shapes as the root. Each declaration kind has one story-wide namespace: paths do not assign scope, ownership, or precedence. All declarations are collected before references resolve, so a character can use an item declared in a different file. The required player character may also be in a supporting file.

Configuration filenames end in case-sensitive .talewright.yaml; unrelated YAML and .yml files are ignored. No additional configuration file is allowed beside the root manifest. The basename story.talewright.yaml is reserved for that single root manifest; using it in a subfolder is an error, not a nested story. Reserve the .talewright.yaml ending for story configuration rather than unrelated tool files. For practical layouts, see Organizing your story. Supporting files cannot contain game:. Duplicate identities are errors naming both sources; there are no includes, overrides, or filename ordering rules. Only one file may declare triggers:, and its list runs in authored order. Trigger order affects behavior; keep the list in the root manifest unless there is a specific reason to move it. Each configuration file contains one YAML mapping document. Folder, ZIP, embedded, and published stories use the same inputs.

Moving declarations preserves their meaning, but paths and text remain part of the source fingerprint. Reorganization is a source change, not a guarantee of compatibility with existing saves. The debugger requires an explicit restart after configuration additions, edits, renames, or deletions.

game:
  id: lost-relic # unique; kebab-case is the recommended style
  title: The Lost Relic
  version: 0.1.0
  start: camp.arrival # first scene id
  randomness: checks-only # off | checks-only (default)
  seed: 1234 # optional; omitted = random per playthrough
  undo: true # default; false hides Back/Forward without disabling saves
  ruleset:
    base: generic # generic | none
    disable: [cha] # remove stats from the base ruleset
    stats: # add stats
      - id: courage
        name: Courage
        category: attribute # attribute | skill | resource | progression | custom
        min: 0
        max: 10
        default: 0

characters:
  player: # the id "player" is required
    first-name: Mara
    last-name: Burns
    pronouns: she # she | he | they | it, or a custom set (see below)
    stats: { dex: 14, athletics: 2 }
    inventory: [rope, torch] # or { torch: 3 }
  guide:
    first-name: Tomas
    pronouns: he

factions:
  explorers-guild: { name: "Explorers' Guild", standing: 0 }

items:
  rope: { name: coil of rope }
  relic: { name: Relic of Ysh, modifiers: { wis: 2 } } # held items modify stats

flags: [door-open] # optional; flags/vars are also auto-declared when an effect sets them
vars:
  torches-lit: 0
  weather: "sunny"

triggers:
  - id: guild-turns
    when: faction:explorers-guild <= hostile
    do: set flag:guild-hostile
    once: true

For a complete custom skill declaration, character value and check, see Add a skill for your setting.

Custom pronoun set:

pronouns:
  {
    subject: xe,
    object: xem,
    possessive-pronoun: { determiner: xyr, independent: xyrs },
    reflexive: xemself,
    plural: false,
  }

Optional character creation

Without creation, the authored protagonist starts unchanged. With it, the shared player requires a legal selection before play. The player declaration remains required and provides identity, inventory and other stat defaults.

game:
  creation:
    name: true
    pronouns: [they, she, he]
    stats:
      str: [8, 12, 16]
    presets:
      explorer:
        label: Explorer
        stats: {str: 12, hp: 15}
      scholar:
        label: Scholar
        stats: {str: 8, hp: 10}

All fields are optional. The first listed pronoun and preset are the defaults.

Field Rules
name: true Players may edit first and last names (trimmed, at most 80 characters each, first name required, no control characters). Fields start with the authored player names; Use default name restores them.
pronouns Distinct built-in keywords. Omit to keep an authored custom pronoun set.
stats Up to 32 stats, each with 1–100 distinct whole-number options within the ruleset’s bounds.
presets Up to 16. Each sets starting stats within ruleset bounds; values for selectable stats must be listed options.

Starting values. For each selectable stat, the default is the preset’s value if the preset sets one, otherwise the authored player value. If that isn’t a listed option, or neither exists, the first option is used. An explicit 0 counts as a value. The player’s own picks override the preset.

During play. Creation happens before the first scene and is replayed identically by Back, Forward and loading. Saves keep the created character, and Restart keeps it too. Reopening the story from the library asks again; loading a save then switches to the saved character.

Not included: point-buy, class designers or race systems.

2. Scenes

=== temple.entrance
@title The Sealed Door
@requires player:dex >= 12 or player has rope
@tags temple
@priority 10
@once

Prose paragraph with **strong**, *emphasis* and `code`.
{player:first-name#possessive} torch gutters as cold air spills out.

> guide: Careful, {player:first-name}. Nobody has opened this in a thousand years.

~ var:torches-lit += 1

* [Read the glyphs] (requires: player:int >= 12) -> temple.glyphs
* [Shove the door] { faction:explorers-guild -= 5 } -> temple.hall
Element Meaning
=== id Starts a scene. IDs use the shared identifier rules; dots may group words, as in camp.arrival.
@title text Display title (used for hub choices).
@requires expr A rule that decides whether a scene is available when using a hub.
@tags a, b Tags for hubs.
@priority n Higher sorts first in hubs (default 0).
@once A scene offered by a hub disappears after the player visits it once.
@hub tag At this scene, offer each available scene with the tag as a choice.
@hub all(tag, tag) Offer scenes that have every listed tag.
@hub any(tag, tag) Offer scenes that have at least one listed tag.
@hub-limit N Cap generated storylet choices at positive integer N; requires @hub.
blank-line separated text Paragraphs (bold, italic and code formatting).
> who: text Speaker line; who must be a character id.
{image("path", "alt text")} Image media in any narrative-template context; alt text is optional. Lookup and display are separate features.
~ effect Effect applied when the scene is entered (before rendering).
* [label] (requires: expr) { effects } -> target Choice. (requires…) and {effects} are optional. Hidden when requires is false.
-> target Scene divert. With no choices, the player sees a single Continue. -> END ends the story.
{if expr} / {else} / {/if} Conditionals — on their own lines (block) or inside a paragraph (inline).

Metadata lines are only recognized directly after the === header. Lines starting with // are comments.

Where a scene goes next. A choice with its own -> target (or a skill check) goes there. A choice with no target — e.g. * [Take the torch] { give player torch } — uses the scene’s own -> target line as its destination. A check outcome without -> target works the same way. A scene must have at least one way out (choices, ->, a ::: skill-check block or @hub), otherwise Talewright reports an error.

Advanced: hub selection. A scene may have only one @hub and one @hub-limit. A hub checks tags, requirements and @once, then sorts matching scenes by priority before applying its limit. Multiple matching tags never duplicate a choice. Explicit choices are not capped. A fallback -> applies only when no explicit or hub choices are available. These details matter when building a story around hubs; you can skip them when using direct choices.

For exact selection behavior: tags must be distinct and nonempty; the hub scene itself is never offered. Higher @priority scenes come first, with ties kept in declaration order. The limit applies only to generated hub choices, not choices written directly in the scene. When checking whether a scene can ever be reached, Talewright considers all matching tag scenes, even if the current limit would hide some; their requirements may change later.

Conditional structure. Choices, ~ effects and -> diverts inside a block {if} only apply when the condition holds (a choice’s own requires is combined with it using and). With several -> lines, the first whose condition holds wins, so put conditional diverts before an unconditional fallback.

Literal braces. Write \{ and \} for literal braces in prose. A line starting with * followed by a space or [ is a choice; *emphasis* at the start of a line is prose.

3. Substitutions

{reference#modifier#modifier…}

Reference Value
player:first-name, player:last-name, player:name Names (name = first + last)
player:pronoun Subject pronoun (she, they)
player:<stat> Effective stat value (base + modifiers)
faction:<id> Player standing with the faction (number)
faction:<id>:tier Standing tier name (friendly, …)
faction:<id>:name, item:<id> Display names
var:<id>, flag:<id> Story variables / flags

Every property separator is :. For example, use faction:explorers-guild:name and faction:explorers-guild:tier. Dots within an ID remain part of that name.

Any character id can replace player.

Modifier Effect
#possessive Grammatically correct English possessive (Chicago style): Mara's, Burns's, the guards'
#capitalize First letter upper case
#upper / #lower Case
#a Prefix with a/an

Pronoun forms. {player:pronoun} gives the subject form. Choose another form with a modifier straight after pronoun, then chain other modifiers after it:

Write she they
{player:pronoun} she they
{player:pronoun#object} her them
{player:pronoun#possessive} her their
{player:pronoun#possessive("independent")} hers theirs
{player:pronoun#reflexive} herself themselves
{player:pronoun#reflexive#capitalize} Herself Themselves

On a pronoun, #possessive picks the declared possessive form instead of adding 's. Its optional argument is "determiner" (the default, used before a noun) or "independent" (stands alone). Custom pronoun sets use the forms they declare under possessive-pronoun: determiner and independent. Both values are required. The old flat possessive and scalar possessive-pronoun fields are errors that name the nested replacement. The older player:pronoun:object and player:pronoun.object spellings are errors that name the replacement. The dotted forms pronoun.subject, pronoun.object, pronoun.possessive, pronoun.possessive-pronoun and pronoun.reflexive are rejected even if a custom stat has the same name.

Images

You unfold {image("maps/old_map-v2", "An old map of the northern coast.")}.

> guide: Look at {image("symbols/crown", "The king's crest")}.
* [Take {image("items/coin", "A silver coin")}] -> END

Both arguments are literal double-quoted strings, separated by a comma. The path is required, relative to the story’s images/ folder, with an optional extension. It is a file path, not a story identifier, so quote it even when it contains no spaces. Alt text is optional: {image("maps/old_map-v2")}.

Inside either argument, \" means a quote and \\ means a backslash. Other backslash escapes are errors. Apostrophes, commas, parentheses, brackets and braces are literal within the quotes. Alt text is not evaluated as a template: "{player:name}" remains that literal text. The parser preserves strings after escape decoding, including empty or whitespace-only alt text. Whitespace is allowed around image, parentheses and arguments.

Images work wherever narrative templates work: prose, dialogue, choice labels, formatting and conditions. There is no image-specific surface allowlist or author image-size limit. Plain-string fields such as scene titles do not gain template support. To show the syntax literally in prose, escape its outer braces: \{image("camp")\}. Code formatting alone does not escape a template. The character reference {image:name} still means the name property of the character image.

Compilation, talewright validate and editor analysis check path safety, supported formats, file existence, ambiguous extensions and missing alt through the shared resolver’s TW2010–TW2014 diagnostics. The editor suggests paths from images/ in the first quoted argument and refreshes suggestions and diagnostics when files change. Lookup reads paths, not image bytes. Omitted, empty or whitespace-only alt warns and uses sentence-case filename fallback. Explicit nonblank alt is preserved after escape decoding. Extensionless paths select .png, .jpg, .jpeg, .webp in that order, warning if more than one matches. Compiled media stores the selected path including images/ and the effective alt text. The native desktop folder player and native folder Author Debugger display these images, as do the built-in samples in the desktop and web players. Images load only when their mounted occurrence approaches the viewport. They fit the available width, retain their aspect ratio, and use the effective alt text. Dialogue, choices, chosen-choice history, styles and conditionals use the same renderer.

Folder images are live files, not a byte snapshot. Within an accepted story generation, occurrences of the same resolved filename share a read and Blob URL while any loaded holder remains. Alt text and style stay independent. After the last holder releases it, a later occurrence reads again and can see edits or fail after deletion. This avoids duplicate copies, but does not bound retention of many distinct images; no persistent cache or eviction is added. The debugger refreshes image occurrences on accepted source/image changes; rejected source keeps the last accepted story and images. Unavailable or undecodable images show their effective alt and a visible, accessible failure. There is no remote fallback.

ZIP stories and browser uploads retain their compressed archive for lazy image access through the same renderer. Untrusted archives enforce defensive expansion accounting, distinct from folder authoring and the browser upload convenience cap. A later image failure preserves the story and shows effective alt plus an accessible error. Published web and desktop hosts also use the shared renderer and include the actual image assets and catalogue. Web publications keep images as external files under story/images/, resolved relative to the application’s base path, including deployment in a subfolder. Serve a published web game through a web server rather than opening its HTML as a local file. Desktop publications use the adjacent story folder. Keep the complete published output together. See sharing and artwork guidance for hosting-size warnings and platform verification limits.

The old ![alt](path) form is an error with a quoted {image("path", "alt")} replacement. The proposed @image(...) form was not shipped and is not supported.

4. Expressions

or / and / not            boolean logic
== != < <= > >=           comparison
+ - * /                   integer arithmetic
player has rope           inventory check (any character id)
visited(temple.hall)      has the scene ever been entered
true false 12 "text"      literals
hostile … allied          reputation tier names (compare with faction:<id>)
@CanPickLock              named predicate registered from C# (see C# extensions)

Types are checked at compile time (number, bool, text, tier). Put spaces around subtraction: player:hp - 3 subtracts three, whereas player:hp-3 refers to a stat named hp-3. Hyphens and dots are allowed inside identifiers. See the operator registry for exact naming rules.

5. Effects

Text variables

Declare text with a quoted initial value under vars in story.talewright.yaml:

vars:
  weather: "sunny"
  remembered-weather: ""
  numeric-label: "12"

Use the same variable references in Talemark:

~ var:remembered-weather = var:weather
~ var:weather = "stormy"
The weather is {var:weather}.
{if var:weather == "stormy"}Find shelter.{/if}
{if var:weather != var:remembered-weather}It has changed.{/if}

A variable’s initial value fixes its type. Text assignment copies the value at that moment, not a live link to the source variable. Empty text and non-ASCII text are preserved. Quoted numbers such as "12" are text, not numbers. Text supports = and case-sensitive == / !=, not arithmetic, +=, -=, ordered comparisons, concatenation, or automatic conversion to/from numbers.

Declaring variables in Talemark

An assignment can introduce a variable without a manifest declaration:

~ var:weather = "stormy"
~ var:remembered-weather = var:weather

Declarations are discovered across the whole story before play, so references in earlier scenes or other files work. Implicit text starts as ""; implicit numbers start at zero. Discovery does not execute the assignment: an unvisited scene cannot change the weather. Assignments only copy values when their effects execute.

Manifest declarations establish types first. Otherwise, the first assignment establishes the type: .tale files are inspected in ordinal path order, effects in source order (left to right within a list), then trigger effects in manifest order. That order does not depend on filesystem enumeration or the reader’s choices. Later incompatible assignments are errors, not type changes.

Types can flow through forward references and assignment chains. Arithmetic and compound assignments require integers. When a chain has no resolvable type (for example, two variables only assigned from each other), the compiler reports TW2007: add an explicit manifest initial value to establish the type.

The current string-literal escape limitations in the operator registry still apply; text variables do not add quote or backslash decoding. See the runnable TextVariables example.

Separated by ; inside { }, one per ~ line.

player:hp -= 3            stat arithmetic (= += -=), clamped to min/max
faction:explorers-guild += 5
var:torches-lit = 2
set flag:door-open        / clear flag:door-open
give player relic         / give player torch 2
take player rope

6. Skill checks

Attached to a choice (outcome lines indented below it):

* [Climb the collapsed wall] skill-check athletics dc 14
  success: -> temple.upper
  failure: { player:hp -= 2 } -> temple.fall

Or as a block that resolves when the scene is entered:

::: skill-check perception dc 12
success: -> temple.hidden-door
failure: -> temple.hall
:::

Optional actor: skill-check guide survival dc 10. Resolution is defined by the ruleset (generic: d20 + attribute modifier + skill rank ≥ DC). Dice are seeded and recorded as events. With randomness: off, checks are deterministic (10 + modifiers ≥ DC).

7. Diagnostics

Every compiler message has a stable code (TW1xxx syntax, TW2xxx names and types, TW3xxx story structure, TW4xxx story.talewright.yaml) and a file:line:column location. See error codes for what each one means and how to fix it.

These docs are maintained with the code in squid-farm/talewright. Found a problem? Open an issue.