A Beginner's Guide to PawScript Expressions

PawScript expressions are little snippets for including live information from your world inside your adventure — a hero's stats, a party, an inventory — and making your writing change with the story.

You can drop them into the instructions you send to the AI — or indeed pretty much anywhere else in the editor. You may already have met PawScript without knowing its name: if you've ever written <<health>> in a piece of text to show the player's health, that was a PawScript expression. And if you haven't, no problem at all — that little <<health>> is exactly where we'll begin. This guide gently builds on that idea, one step at a time, until you can do things like "list every party member above level 3" or "show the player's gold with a comma in it."

A key note first. That <<health>> example above will only work if you have a tracked item called "health". The tracked items come first, and then you can write expressions that make use of them.

Take your time. Nothing here is hard on its own — it's all small pieces that snap together. And nothing you've already written will break: any old <<health>> expressions keep working exactly as before, with nothing to convert.

One language, two forms. PawScript comes in two forms: expressions — small <<…>> snippets that can be used to return a value or to insert a value into your text — and scripts, which can be more than one line and are used to change aspects of the game, such as the values of your Tracked Items. This guide teaches expressions; everything you learn here carries straight over to scripts, which have their own guide.

See also: the YAML guide covers how to store structured data — lists and labelled blocks. PawScript is how you read that data back out. You don't need YAML to begin, though: PawScript works just as happily on a simple value like a single health number, so this guide stands on its own.

Try as you read: open the PawScript Expression Playground in a new tab to test expressions against sample data while you follow along.



What is a PawScript expression?

In one breath: an expression is a little snippet that shows a live value in your writing, reshaped so it reads nicely.

Without an expression, you'd have to hand-write a fixed sentence:

"You have 30 hit points left."

…which is always the same, no matter what's actually happening in the game.

With an expression, you write the idea and let the game fill in the real number:

"You have <<$health>> hit points left."

…which becomes "You have 18 hit points left." or "You have 5 hit points left." depending on how the hero is doing right now.

A few things to know up front, so nothing surprises you later:


The golden rule: everything goes inside double brackets

Every expression goes between double angle brackets — two less-than signs to open, two greater-than signs to close. That's how the game knows "work this bit out and replace it." Anywhere you can write adventure text — narrative, a tracked item's starting value, a trigger's condition, an AI prompt — you can drop one in.

The simple rule of thumb: if the editor already lets you type <<health>> there today, you can write any expression there.

(Scripts — PawScript's other form — are the one exception to the brackets rule: a script lives in its own box in the "Run a script" trigger effect, so it needs no brackets at all.)

Everything outside the brackets is just ordinary text, left exactly as you wrote it.

And here's the reassuring part: you can't break anything by experimenting. A PawScript expression only reads values, so a mistake never damages your world — at worst an expression simply shows nothing. When you want to check an expression before using it, the PawScript Expression Playground has an Evaluate button that runs it against your data and explains, in plain language, anything that's wrong.


The world we'll use

So the examples feel real, we'll use one little world all the way through. Picture these as tracked items you've already set up. (The blocks below are written exactly as you'd paste them into the PawScript Expression Playground's data box, if you want to follow along.)

The hero, Alice (a few simple tracked items):

$health: 18
$max_health: 30
$gold: 1240
$level: 3

Alice's companions — a list called $party, where each member is a small block of labels. (Alice is the hero above; these are the three who travel with her.)

$party:
- name: Mira
  class: Wizard
  health: 18
  level: 3
  nickname: Em
- name: Bjorn
  class: Warrior
  health: 30
  level: 5
- name: Cora
  class: Rogue
  health: 22
  level: 4
  nickname: Shadow

The inventory — a labelled block called $inventory (what the YAML guide called "labelled information": each label — here, each item's name — leads to its own little description underneath).

$inventory:
  sword:
    damage: 8
    weight: 5
    type: weapon
    count: 1
  shield:
    defense: 4
    weight: 7
    type: armor
    count: 1
  potion:
    heals: 20
    weight: 1
    type: consumable
    count: 3
  antidote:
    weight: 1
    type: consumable
    count: 2

Don't worry about memorising any of this — just glance back here whenever an example mentions Alice or the sword.


Step 1: Show a value

The simplest expression shows a single tracked item:

<<$health>>

This becomes 18 — Alice's current health. Read the $ as "the tracked item called…", so $health is "the tracked item called health."

You may also have seen it written without the $:

<<health>>

Both give 18, but the $ form is the standard form going forwards — it's what this guide, the editor's autocomplete, and all the examples use from here on. The bare form is an older style that's been kept working for compatibility, so nothing you've already written breaks; for anything new, reach for the $. It also pays its way in longer expressions, where it makes it instantly clear you're pointing at a tracked item.

A note on names. Tracked items have a friendly name you typed (like "Max Health") and a behind-the-scenes name you use in expressions. The behind-the-scenes name is the friendly one in lowercase with spaces turned into underscores:

Friendly name What you write
Health $health
Max Health $max_health
Gold Pieces $gold_pieces

If you later rename the friendly name, the behind-the-scenes name stays the same — so your existing expressions keep working rather than silently breaking. You can see the behind-the-scenes name (and choose to update it to match a new friendly name) on the (i) button next to the tracked item's name in the editor.


Step 2: Do a quick sum

PawScript can do arithmetic. The usual symbols all work: +, -, * (multiply), / (divide), ^ (power), and % (the remainder after dividing).

<<$health + 10>>      → 28
<<$gold - 200>>       → 1040
<<$gold * 2>>         → 2480
<<10 % 3>>            → 1
<<2 ^ 3>>             → 8

You can combine items and wrap the result in a tool like round() to tidy it up. Here's a health percentage:

<<round($health / $max_health * 100)>>

That works out 18 / 30 * 100, which is 60, and round() keeps it a clean whole number: 60.

There are a handful of maths tools you can use this way: round, round_down, round_up, abs (strip a minus sign), min, max, power, and the trig tools sin, cos, tan. There's also random(), which we'll meet with the dice.


Step 3: Make a choice

The if() tool picks between two values based on a test. You give it three things: a test, the value to use if the test is true, and the value to use if it's false.

<<if($health > 50, "healthy", "wounded")>>

Alice's health is 18, which is not more than 50, so this becomes wounded.

It's perfect for a line that should only appear sometimes. Here, the warning shows nothing while the hero is fine, and appears the moment things get dangerous:

You have <<$health>> HP. <<if($health < 10, "You are badly hurt!", "")>>

At 18 HP the second part is empty, so you just see "You have 18 HP." If health drops below 10, the warning pops in. Notice the "do nothing" option is just an empty pair of quotes "".


Step 4: Sensible fallbacks

Often you want "use this if it's set, otherwise fall back to that." PawScript gives you three friendly tools for this — think of them as the fallback family.

first_available(...) walks through your values left to right and returns the first one that actually has something in it:

<<first_available($nickname, $name, "Adventurer")>>

Alice's $nickname is empty, so it skips to $name and returns Alice. If a value can't be worked out at all — say it points at a detail that isn't there — it's simply skipped too, so it's safe to list things that might be missing.

choose(...) matches a value against options, like a multiple-choice lookup. You give it a value, then pairs of match → result, and a final default:

<<choose($level, 1, "novice", 3, "adept", "master")>>

Alice is level 3, which matches the second pair, so this becomes adept. If nothing matched, the final lone value — "master" — would be the default.

try(...) runs an expression but quietly uses a fallback if anything goes wrong:

<<try($inventory.axe.damage, "no axe")>>

There's no axe in the inventory, so rather than complain, it returns no axe.


Step 5: Stitch values into a sentence

When you want to mix several values into one piece of text, format_text() is the tidiest tool. Write your sentence with {...} gaps, and PawScript fills each gap in:

<<format_text("HP: {$health}/{$max_health}")>>

becomes HP: 18/30. Anything in {curly braces} is worked out; everything else is kept exactly as written. You'll reach for this a lot once your sentences start mixing two or three values together.


Step 6: Reach into your data

In the YAML guide you learned to build structured data. Now let's reach into it. You walk inwards with dots, one level at a time.

<<$inventory.sword.damage>>     → 8
<<$inventory.potion.count>>     → 3

Read the dots left to right: "in inventory, find sword, then its damage." Each dot takes you one step deeper.

Labels with spaces — quote them

Most labels are single words, so a plain dot is all you need. But some have spaces or punctuation — My Name, Dragon's Hoard. A bare dot can't read those (PawScript stops at the space), so wrap the label in quotes right after the dot:

<<$names_list."My Name">>      → Alice
<<$names_list."Their Name">>   → Bjorn

The quotes just say "this whole thing is one label" — what's inside is matched exactly, spaces and all. Use double quotes normally, or single quotes when the label itself contains a double quote: .'She said "hi"' and ."O'Brien" both work. (If you forget and type .My_Name, PawScript spots the near-miss and points you at the quoted form.)

Picking an item from a list

For a list like $party, you pick by position (starting at 1, just like everyday counting), or grab the ends:

<<$party.item(2).name>>     → Bjorn
<<$party.first().name>>     → Mira
<<$party.last().name>>      → Cora
<<$party.count()>>          → 3

.count() tells you how many there are. (Notice the brackets — .count() with brackets uses the counting tool. We'll come back to why brackets matter in Troubleshooting.)

Checking something is there — .exists()

.exists() answers "does this path lead anywhere?" without ever erroring, even if it doesn't:

<<$inventory.sword.exists()>>     → true
<<$inventory.axe.exists()>>       → false

It pairs beautifully with if(): <<if($inventory.sword.exists(), "You grip your sword.", "Your hand finds nothing.")>>.

An aside: two built-in values, $player and $game

You don't need these to continue — but it's handy to know that a couple of values are always there for you, supplied by the game itself rather than by a tracked item you made:

<<$player.name>>                  → Alice
<<$player.skills.fire_magic>>     → 5
<<$game.turn_number>>             → 7
<<$game.ai_model>>                → Gryphon

($game.ai_model is the name of the AI model the player has chosen from the main menu — handy if you want your world to read differently depending on which storyteller is running it.)

Because they're built in, the names player and game are spoken for — the editor won't let you call one of your own tracked items player or game.


Step 7: Work with a whole list

Here's the idea that unlocks everything else. When you have a list like $party and you want one detail from every member, you add .all(), then the detail:

<<$party.all().name>>     → ["Mira", "Bjorn", "Cora"]
<<$party.all().health>>   → [18, 30, 22]

.all().name means "for every member, take its name." You get back a list of all three names.

That ["Mira", "Bjorn", "Cora"] is simply how we show "a list of three things" on the page: the square brackets mean "a list", and the quotes mean "these are words." You won't see the brackets in your game — in Step 11 you'll turn a list like this into a tidy sentence such as "Mira, Bjorn and Cora."

Why the .all()? Because <<$party.name>> would be ambiguous — does it mean "the party's own name" or "every member's name"? So PawScript asks you to be clear. Writing .all() plainly says "do this for every member." If you forget it and test the expression in the Playground, you'll get a clear message telling you to add it.

One nice touch: if some members are missing the detail, they're simply skipped. Only Mira and Cora have nicknames:

<<$party.all().nickname>>     → ["Em", "Shadow"]

Step 8: Pick out the ones you want

.where(...) keeps only the items that match a test, and drops the rest. Inside the brackets you write a test, naming the fields plainly — and note there's no $ here, because you're already "inside" each item, so you just say level, not $level:

<<$party.where(level > 3).name>>     → ["Bjorn", "Cora"]

That keeps the members above level 3, then takes their names. (After .where(...) you can go straight to the detail — no .all() needed, because .where(...) has already picked out the items.) You can pick out a single match with .first():

<<$party.where(class = "Wizard").first().name>>     → Mira

On a labelled block like $inventory, .where(...) keeps matching entries, and .keys() gives you their names:

<<$inventory.where(type = "weapon").keys()>>     → ["sword"]

And when a list holds plain values rather than blocks of labels — say $visited is just the list [Bree, Rivendell, Moria] — there are no field names to test. Use value() to mean "the item itself":

<<$visited.where(value() != "Bree")>>     → ["Rivendell", "Moria"]

The tests you can use

Inside .where(...) (and the yes/no tools in Step 9), you can use:

<<$party.where(name contains "or").name>>     → ["Bjorn", "Cora"]

A small rule worth remembering: after .where(...) you can go straight to a detail (as above). After a reordering step like .sort_by(...) or .reverse(), add .all() before the detail. If in doubt, add .all() — it's never wrong, only sometimes unnecessary.

For a richer real-world filter — "which skills could I learn next?", combining not in and all in — see the skill-tree recipe in the Cookbook, which sets up the extra tracked items it needs.


Step 9: Quick yes/no questions

Sometimes you just want a true-or-false answer about a whole collection. Three tools give it to you, using the same tests as .where():

<<$party.any(class = "Wizard")>>          → true
<<$party.every(health > 0)>>              → true
<<$party.none(class = "Necromancer")>>    → true

These read wonderfully inside if(): <<if($party.any(class = "Wizard"), "A wizard travels with you.", "No magic here.")>>.


Step 10: Count things up and combine them

PawScript can total, average, and find the biggest or smallest across a collection.

<<$party.sum(health)>>      → 70
<<$party.average(level)>>   → 4.0
<<$party.max(level)>>       → 5

You name the field inside the brackets. You can even put a little calculation in there — "how many wizards?" by adding 1 for each:

<<$party.sum(if(class = "Wizard", 1, 0))>>     → 1

.max(level) gives you the highest value. If instead you want the member with the highest value, use .max_by(...) (and .min_by(...)):

<<$party.max_by(level).name>>     → Bjorn
<<$party.min_by(health).name>>    → Mira

Sorting

.sort_by(field) puts a list in order; add the word desc for highest-first. Use .then_by(...) as a tie-breaker:

<<$party.sort_by(level, desc).all().name>>                 → ["Bjorn", "Cora", "Mira"]
<<$party.sort_by(class).then_by(level, desc).all().name>>  → ["Cora", "Bjorn", "Mira"]

The second one sorts by class first (Rogue, Warrior, Wizard), and within each class breaks ties by level, highest first.


Step 11: Make things look nice

This step is a buffet, not a ladder — skim it and grab what you need. You don't have to learn it all; just remember it's here for when you want to polish a value.

Tidying text

<<$name.upper()>>                   → ALICE
<<"hello world".title_case()>>      → Hello World
<<$name.truncate(3)>>               → Ali
<<$name.truncate(3, "...")>>        → Ali...
<<"a,b,c".split(",")>>              → ["a", "b", "c"]

There's also .lower(), .trim() (remove surrounding spaces), and .replace(old, new).

Emphasis

These wrap text so the game renders it with styling:

<<$name.bold()>>      → **Alice**   (shows as bold in the game)
<<$name.italic()>>    → *Alice*     (shows as italic)

…plus .strikethrough() and .code().

Numbers

<<$price.round()>>            → 13       (price is 12.6)
<<$price.round(1)>>           → 12.6
<<$price.round_down()>>       → 12
<<$price.round_up()>>         → 13
<<$gold.format_number()>>     → 1,240    (commas for big numbers)
<<$ratio.format_percent()>>   → 85%      (ratio is 0.85)
<<$ratio.format_percent(1)>>  → 85.0%
<<$rank.ordinal()>>           → 3rd      (rank is 3)
<<$balance.abs()>>            → 50       (balance is -50)
<<$health.constrain(0, 20)>>  → 18       (keeps a value between two bounds)

A handy tip: write number tools on a tracked item (<<$price.round()>>), not on a typed-in number. <<12.6.round()>> won't work, because PawScript reads 12.6 as the number and then trips over the extra dot.

Lists into a sentence

Remember those lists from Steps 7–10? Here's how you turn them into readable text:

<<$names.sort().join(", ")>>              → Bjorn, Cora, Mira
<<$names.sort().join(", ", " and ")>>     → Bjorn, Cora and Mira
<<$inventory.keys().numbered_list()>>

The last one produces a numbered list, each on its own line:

1. sword
2. shield
3. potion
4. antidote

There's also .bulleted_list(), .unique() (remove duplicates), and .reverse().

One template per item — .format_each()

When you want to format every item the same way, .format_each() renders a little template once per item — it's format_text from Step 5, applied to each item in turn. Inside the {...} you can use the item's fields, plus index() for its position:

<<$party.format_each("{name} the {class}").join(", ")>>

becomes Mira the Wizard, Bjorn the Warrior, Cora the Rogue. Number them by adding index():

<<$party.format_each("{index()}. {name}").join("\n")>>

gives a clean roster:

1. Mira
2. Bjorn
3. Cora

For a labelled block, {key()} is the entry's name, and you can reach the value's own fields directly — so just {count}, not {value().count}. ({value()} comes into its own when the items are plain values rather than blocks of fields — there it means "the item itself", just like in .where(...).)

<<$inventory.format_each("{key()} ({count})").bulleted_list()>>

- sword (1)
- shield (1)
- potion (3)
- antidote (2)

Step 12: Dates and times

If a tracked item holds a date, .format_date(...) turns it into friendly text. You build the look you want out of little keywords like day and year; anything that isn't a keyword (spaces, commas, slashes) is kept as a separator.

Using a quest date of Wednesday 8 April 2026, 2:07 PM:

<<$quest_date.format_date("day monthname year")>>            → 8 April 2026
<<$quest_date.format_date("weekday, day monthname3 year")>>  → Wednesday, 8 Apr 2026
<<$quest_date.format_date("hour2:minute2 AMPM")>>            → 02:07 PM
<<$quest_date.format_date("hour24:minute2")>>                → 14:07

The full list of keywords (day, monthname, weekday, hour, and so on) is in the (i) info popup beside .format_date in the editor.

For right now, two tools give you the current real-world time, ready to pass into .format_date(...):

<<now_utc().format_date("day monthname year")>>     (always universal time)
<<now_local().format_date("hour24:minute2")>>       (the player's own timezone)

Step 13: Roll some dice

PawScript speaks classic dice notation, so chance is easy. 1d6 is the tabletop way to write "roll one six-sided die" — the number before the d is how many dice, the number after is how many sides:

<<1d6>>        roll one six-sided die (1–6)
<<3d6>>        roll three six-sided dice (3–18)
<<d20>>        shorthand for 1d20
<<d6(3)>>      another way to write 3d6

If you prefer words to dice notation, dice_roll does exactly the same thing: dice_roll(6, 3) is 3d6, dice_roll(20) is d20, and a bare dice_roll() rolls one six-sided die. Unlike the d shorthand, it appears in autocomplete and the function reference, so it's easy to find:

<<dice_roll(6, 3)>>            three six-sided dice (3–18)
<<$health + dice_roll(6, 3)>>    add three dice to a value

You can mix dice into sums, like an attack roll:

<<1d20 + $strength>>

And random() gives a random fraction between 0 and 1 — handy for "percent chance" tests:

<<if(random() < 0.5, "heads", "tails")>>     a coin flip
<<round(random() * 100)>>                    a number from 0 to 99

Troubleshooting: the few mistakes everyone makes

Don't worry about getting these right from memory. If an expression misbehaves, paste it into the PawScript Expression Playground and press Evaluate — it runs the expression against your data and shows a friendly message explaining exactly what's wrong. Here are the slips you're most likely to bump into.

1. Forgetting the brackets on a tool

Tools like count, upper, and keys need brackets to use them. .count() counts; .count (no brackets) looks for a field named "count."

<<$party.count>>     → "Cannot use '.count' directly on a list — did you mean '.count()'?"
<<$party.count()>>   → 3

If you see a "did you mean …()?" message, you've just forgotten a pair of brackets.

2. Asking for a detail straight off a list

A list doesn't have a name — each item does. So add .all() (for every item) or .first() (for one):

<<$party.name>>          → "Cannot access '.name' directly on a list — use .all().name…"
<<$party.all().name>>    → ["Mira", "Bjorn", "Cora"]
<<$party.first().name>>  → Mira

3. An expression that comes out empty

If you get nothing back, it's usually one of:

Use .count() to investigate: <<$party.where(level > 100).count()>> will tell you how many matched (probably 0).

4. A number turning into text

When a number meets a text tool, it quietly becomes text:

<<$health.upper()>>     → 18

That's usually fine. If you need it to stay a number for a sum, just don't pass it through a text tool.


Cookbook: worked examples

Real recipes you can adapt, each one tested against the world above. Skim for the shape that matches what you're trying to do.

A few recipes below use extra tracked items we haven't shown in full (a family tree, a quest log, some simple lists). Don't worry about their exact contents — focus on the shape of the expression, which works the same on your own data.

A party (a list of objects)

List everyone's name.
<<$party.all().name>>                              → ["Mira", "Bjorn", "Cora"]

How many are there?
<<$party.count()>>                                 → 3

The highest-level member's name.
<<$party.sort_by(level, desc).first().name>>       → Bjorn

Members above level 3.
<<$party.where(level > 3).name>>                   → ["Bjorn", "Cora"]

Total health of the whole party.
<<$party.sum(health)>>                             → 70

Everyone's nickname (some don't have one — they're skipped).
<<$party.all().nickname>>                          → ["Em", "Shadow"]

Every name in bold, joined with commas.
<<$party.all().name.bold().join(", ")>>            → **Mira**, **Bjorn**, **Cora**

An inventory (a labelled block of items)

The sword's damage.
<<$inventory.sword.damage>>                           → 8

List every item name.
<<$inventory.keys()>>                                 → ["sword", "shield", "potion", "antidote"]

Just the weapons.
<<$inventory.where(type = "weapon").keys()>>          → ["sword"]

Total weight carried.
<<$inventory.sum(weight)>>                            → 14

How many consumables (3 + 2)?
<<$inventory.where(type = "consumable").sum(count)>>  → 5

Every distinct item type.
<<$inventory.all().type.unique()>>                    → ["weapon", "armor", "consumable"]

A bulleted list with counts.
<<$inventory.format_each("{key()} ({count})").bulleted_list()>>
→
- sword (1)
- shield (1)
- potion (3)
- antidote (2)

A skill tree (nesting + dependencies)

# $skill_tree
skills:
  - id: flame_dart
    name: Flame Dart
    depends_on: []
  - id: fireball
    name: Fireball
    depends_on: [flame_dart]
  - id: frost_nova
    name: Frost Nova
    depends_on: [flame_dart]
  - id: inferno
    name: Inferno
    depends_on: [fireball, frost_nova]
# $unlocked: [flame_dart, fireball]
All skill names.
<<$skill_tree.skills.all().name>>                             → ["Flame Dart", "Fireball", "Frost Nova", "Inferno"]

Skills not yet learned.
<<$skill_tree.skills.where(id not in $unlocked).name>>        → ["Frost Nova", "Inferno"]

Skills you could learn next (prerequisites met, not yet learned).
<<$skill_tree.skills.where(id not in $unlocked and depends_on all in $unlocked).name>>
→ ["Frost Nova"]

Progress as "2 of 4 learned".
<<format_text("{$unlocked.count()} of {$skill_tree.skills.count()} learned")>>   → 2 of 4 learned

A family tree (deep nesting)

# $family — Rosa, her children, and their children
name: Rosa
age: 70
children:
  - name: Tomas
    age: 45
    children:
      - name: Pip
        age: 20
      - name: Bea
        age: 18
  - name: Lena
    age: 42
    children:
      - name: Otto
        age: 15
Rosa's own children.
<<$family.children.all().name>>                                → ["Tomas", "Lena"]

Every descendant, however deep.
<<$family.children.descendants().name>>                        → ["Tomas", "Pip", "Bea", "Lena", "Otto"]

The average age of all descendants.
<<$family.children.descendants().average(age)>>                → 28.0

Descendants over 30.
<<$family.children.descendants().where(age > 30).name>>        → ["Tomas", "Lena"]

How many descendants in total?
<<$family.children.descendants().count()>>                     → 5

.descendants() is the tool for "everything nested inside, at any depth" — skill trees, family trees, nested menus.

A quest log (mixed shapes)

Active quests, by priority.
<<$quest_log.where(status = "active").sort_by(priority).all().title>>   → ["The Dark Tower", "The Merchant's Plea"]

Every unfinished objective across all quests.
<<$quest_log.all().objectives.flatten().where(not complete).description>>
→ ["Cross the Shadowlands", "Escort to Riverton"]

Each quest with its status.
<<$quest_log.all().format_each("{title} ({status})")>>
→ ["The Dark Tower (active)", "Lost Kitten (complete)", "The Merchant's Plea (active)"]

.flatten() joins several small lists (each quest's objectives) into one big list.

Simple collections

A list joined with "and".
<<$simple_list.join(" and ")>>          → apple and banana and cherry

The total of some numbers.
<<$numbers.sum()>>                      → 45

Numbers sorted high to low.
<<$numbers.sort().reverse()>>           → [18, 12, 7, 5, 3]

The values of a name→nickname labelled block.
<<$nicknames.values()>>                 → ["Mira", "Bjorn", "Cora"]

Does the labelled block have a "wolf" entry?
<<$nicknames.keys().contains("wolf")>>  → true

Is "Bjorn" one of the nicknames? (.in() reads subject-first — the reverse of contains)
<<"Bjorn".in($nicknames.values())>>     → true

Cheat sheet

This is the quick version. For the exhaustive list — every function and operator, each with examples — see the PawScript Reference.

Show a value

<<$health>>            a tracked item
<<$player.name>>       a built-in value
<<format_text("HP: {$health}/{$max_health}")>>   stitch values into a sentence

Decide

<<if(test, yes, no)>>
<<first_available(a, b, "default")>>     first one that has a value
<<choose($x, match1, result1, ..., default)>>
<<try(expression, "fallback")>>

Reach in

<<$inventory.sword.damage>>     dots go deeper
<<$names_list."My Name">>        quote a label with spaces
<<$party.item(2)>>              by position (starts at 1)
<<$party.first()>>  <<$party.last()>>  <<$party.count()>>
<<$path.exists()>>              "true" / "false", never errors

Whole lists

<<$party.all().name>>           a field from every item
<<$party.where(level > 3)>>     keep the matching ones
<<$inventory.keys()>>  <<$inventory.values()>>

Tests (inside .where / .any / .every / .none)

=  !=  >  <  >=  <=
contains  starts_with  ends_with
in   all in   any in   none in
and  or  not

Count & combine

<<$party.sum(health)>>  <<$party.average(level)>>  <<$party.max(level)>>
<<$party.max_by(level)>>        the item, not just the value
<<$party.sort_by(level, desc)>>  then  <<...then_by(field)>>

Make it nice

Text:    .upper()  .lower()  .title_case()  .trim()  .truncate(n)  .replace(a, b)
Style:   .bold()  .italic()  .strikethrough()  .code()
Number:  .round()  .round_down()  .round_up()  .format_number()  .format_percent()  .ordinal()  .constrain(lo, hi)
List:    .join(", ")  .numbered_list()  .bulleted_list()  .sort()  .unique()  .reverse()
Each:    .format_each("{name}: {health}")

Dates & dice

<<$date.format_date("day monthname year")>>
<<now_utc()>>  <<now_local()>>
<<1d6>>  <<3d6>>  <<d20>>  <<random()>>

You're ready

That's the whole of expressions: show a value, do a sum, make a choice, reach into your data, work across a list, filter it, count it, and dress it up. Everything else is just snapping these pieces together into bigger shapes.

The best way to make it stick is to play. Open the PawScript Expression Playground, paste in anything from this guide, and watch it work — then change a number and watch it change. That's how all of this stops feeling like "code" and starts feeling like another tool in your worldbuilding kit.

Ready for the other form? Expressions show values; when you want to change them — award gold, heal the party, tick off a quest — that's a script. Start with Writing Scripts in PawScript.

Happy worldbuilding!