PawScript Reference
Every PawScript variable, statement, operator and function, with examples.
PawScript comes in two forms — expressions (small <<…>> snippets that return a value or insert a value into your text) and scripts (usually more than one line, used to change aspects of the game, such as the values of your Tracked Items). This page covers both: anything marked scripts only works only in a script.
New to PawScript? Start with the Beginner’s Guide for expressions or Writing Scripts in PawScript for scripts.
This page is the complete lookup for when you know roughly what you want. Try expressions in the PawScript Expression Playground, and scripts in the PawScript Playground.
Nothing matches that filter.
Variables
The values a script or expression reads. There are two kinds: native variables the game always provides ($player and $game), and tracked items that you create in your world.
$player
The player character — always available.
$player:
name: "Aragorn"
skills:
swordsmanship: 5
archery: 3
skills_and_levels: "Swordsmanship (level 5), Archery (level 3)"The player character's name.
$player.name→ "Aragorn"
A map of each adventure skill (by its slug) to the player's level in it. Skills the player hasn't raised default to 3. Use $game.skill_map to turn a slug back into its display name.
$player.skills.swordsmanship→ 5$player.skills.count()→ 3 — how many skills there are
Comma-separated list of every adventure skill with the player's current level in parentheses. Useful in AI-prompt templates.
$player.skills_and_levels→ "Tiger Ecology Knowledge (level 0), Community Relations (level 0), Survival Skills (level 0), Negotiation (level 0), Physical Endurance (level 0)"
$game
The current game state — always available.
$game:
turn_number: 12
ai_model: "Gryphon"
skill_list: "Swordsmanship, Archery, Magic"
difficulty_list: "Trivial, Easy, Medium, Hard, Legendary"
skill_map:
swordsmanship: "Swordsmanship"
archery: "Archery"
magic: "Magic"
skill_example: "Swordsmanship"
difficulty_example: "Medium"Name of the AI model the player has chosen, as shown in the main menu.
$game.ai_model→ "Gryphon"
An example difficulty description (Medium). Used by prompt templates.
$game.difficulty_example→ "Medium"
Comma-separated list of difficulty descriptions used by the skill system.
$game.difficulty_list→ "Trivial, Easy, Medium, Hard, Legendary"
First skill name (in adventure order) — handy for prompt templates that show one example skill.
$game.skill_example→ "Swordsmanship"
Comma-separated list of every skill defined for this adventure.
$game.skill_list→ "Swordsmanship, Archery, Magic"
Map from skill slug to its display name. Useful when you've got a slug from $player.skills and need the friendly name back.
$game.skill_map.swordsmanship→ "Swordsmanship" — Looking up a single skill's name
Current turn number, starting at 1 on the opening turn.
$game.turn_number→ 12 — Twelfth turn of the game
Tracked items
Everything else you reference — $party, $gold, $inventory — is a tracked item you define in your world. You choose its name and shape (a number, some text, or a structured YAML value) and read or change it the same way as the native variables: <<$gold>>, $party.count(), $inventory.item(1).name. The native names above are read-only; your tracked items are the things a script exists to change.
Statements
The building blocks of a script — the lines that loop, decide and remember. Statements only exist in scripts; they never appear inside a <<…>> expression.
Repeat the indented lines once for every item in a list or map
Loops over a list or a map, one item at a time. The indented lines underneath run once per item, with the loop variable holding the current one — so $item.hp += 5 really changes that item. To repeat something a set number of times, loop over range(n).
for each $member in $party $member.hp += 5— Everyone in the party heals 5 hp
Run the indented lines only when a condition is true
Checks a condition and runs the indented lines underneath only when it is true. Conditions are ordinary PawScript expressions — $gold >= 50, $party.count() > 2, and so on — and you can join them with and / or.
if $xp >= 100 $level += 1 $xp -= 100— Level up at 100 xp
Try another condition when the if above didn't match
Chains onto an if at the same indentation. Its condition is only checked when everything above it didn't match, and you can add as many as you need.
if $hp > 50 $status = "healthy" else if $hp > 20 $status = "hurt" else $status = "critical"
What to do when nothing above matched
Closes an if chain at the same indentation — its lines run only when none of the checks above matched.
if $gold >= 120 $gold -= 120 else $message = "Too pricey!"
Make a temporary variable that lasts only for this run
Creates a script-local variable — a scratch value that lives only while the script runs and is never saved. Handy for adding things up. set can't name a tracked item — assign those directly with $item = ….
set $total = 0 for each $m in $party set $total = $total + $m.hp $team_hp = $total— Total the party's hp into a tracked item
Operators
Symbols and keywords that combine or compare values. The comparison and logical ones, plus the filter keywords below, are what you use inside .where(...), if(...) and the yes/no tests.
Arithmetic
Add (numbers)
Adds two numbers together. Note, to join two pieces of text, use & (or format_text()) — + is only for numbers.
5 + 3→ 8
Subtract
Subtracts the right value from the left.
10 - 4→ 6$health - 1→ "3" — When $health is 4
Multiply
Multiplies two numbers.
6 * 7→ 42
Divide
Divides the left value by the right. Returns a decimal if the result isn't a whole number — wrap with .round() or .format_number() if you want to control how it's displayed.
10 / 4→ 2.5(10 / 4).round()→ 3
Remainder after division
The "modulo" operator — the leftover after dividing left by right. Useful for "is this a multiple of N?" tests, or for cycling through values on each turn.
10 % 3→ 1$turn_number % 2 = 0→ true or false — Even turn?
Power, e.g. 4^2 = 16
Raises the left value to the power of the right. Two-cubed is 2^3 = 8, the area of a square is $side^2.
4^2→ 162^10→ 1024
Text
Join text together
Joins values into one piece of text ("concatenation"). Both sides are turned into text first, so you can mix words and numbers freely — "HP " & $health becomes "HP 20". It binds looser than the maths operators, so 1 + 2 & " points" is "3 points". For longer sentences with several values, format_text() is often tidier. In a script, &= appends to a text value — $log &= " ...and then we fought.".
"Hello, " & $player.name→ "Hello, Aragorn"$gold & " coins"→ "1234 coins" — Numbers turn into text automatically.$first & " " & $last→ "Frodo Baggins"
Comparison
Equal to
True if both sides are the same value. Works for numbers, strings, booleans. Use inside if(...) or .where(...).
$health = 0→ true or false — Player out of health?
Not equal to
True if the two sides differ.
$location != "tavern"→ true or false
Less than
True if the left value is strictly less than the right.
$health < 3→ true or false — Player low on health?
Greater than
True if the left value is strictly greater than the right.
$gold > 100→ true or false
Less than or equal
True if the left value is less than or equal to the right.
$turn_number <= 10→ true or false — Within the first ten turns?
Greater than or equal
True if the left value is greater than or equal to the right.
$player.skills.swordsmanship >= 5→ true or false
Logical
Both conditions must be true
Combines two truthy expressions — the result is true only if both sides are true. Short-circuits: if the left is false, the right isn't evaluated.
$health > 0 and $turn_number < 20→ true or false
Either condition can be true
The result is true if either side is true. Short-circuits: if the left is true, the right isn't evaluated.
$location = "tavern" or $location = "inn"→ true or false
Negation — flips true to false and vice versa
Inverts a truthy expression. There's also a .not() chain method that does the same thing — not $foo and $foo.not() are interchangeable.
not $skills.contains("archery")→ true or false — Same as $skills.contains("archery").not()
Test whether a value is in a list (inside a filter)
Used inside a filter condition — .where(...), .any(...), .every(...) or .none(...) — to test whether a value appears in a list. The right side must be a list. (The bare word in only works inside those filters. Outside a filter, use the .in() method — $skill.in($unlocked) — or $unlocked.contains($skill).) To check whether a map has a particular key, use .contains("key") on the map instead.
$skills.where(id in $unlocked)→ just the unlocked skills — Keep the items whose `id` is one of the values in $unlocked.$party.any(class in $caster_classes)→ true or false — Works the same in .any(), .every() and .none().
Filter keywords
These only appear inside a filter — .where(...), .any(...), .every(...), .none(...):
in— the value is one of the items in a listany in/all in/none in— for a field that is itself a list: do any / all / none of its items appear in another listcontains/starts_with/ends_with— text tests on a field
Functions
Top-level functions are written on their own (round(...)); the rest are chained onto a value with a dot ($name.upper()). Remember: brackets mean “use the function”, so .count() counts, while .count looks for a field called count.
Top-level
Fill in a template with values
Take a piece of text with {placeholders} in it and fill each placeholder in with the value of an expression. Handy for sentences that mix several variables with fixed wording. Use {$variable} for a variable, or {expression} for a calculation.
format_text("HP: {$health} / {$max_hp}")→ "HP: 20 / 50" — A simple stat lineformat_text("Welcome, {$player.name}. I see you wield your {$chosen_weapon}!")→ "Welcome, Aragorn. I see you wield your Doom Bringer!" — Pulls fields from the player record and a tracked item.
Pick one of two values based on a test
Test the first value; if it's true, return the second value, otherwise return the third. Useful for sentences that change depending on game state.
if($health > 0, "alive", "fallen")→ "alive" — Branch on a comparisonif($game.turn_number > 10, "late game", "early game")→ "late game" — Branch on the turn counter
The first value that has something in it
Walk through the values left to right and return the first one that exists and isn't blank. A value that can't be worked out — because a field or item is missing — is simply skipped so it's safe to list paths that might not exist. Handy for fall-through defaults: try a nickname, then a name, then "Stranger".
first_available($pc.nickname, $pc.name, "Stranger")→ "Strider" — Nickname if set, otherwise the name, otherwise the default — even if a field is missing
Match a value against options, like a multi-way switch
Compare the first value against each "match" and return the "result" paired with the first one that's equal. If nothing matches, return the final unpaired value as a default. Handy for mapping a class, difficulty, or status to a wording.
choose($class, "wizard", "Staff", "ranger", "Bow", "Fists")→ "Bow" — When $class is "ranger". "Fists" is the default if nothing matches.
Try an expression, fall back if it fails
Evaluate the first expression; if anything goes wrong (missing variable, divide-by-zero, type error, etc.), use the fallback instead. Stops a single bad reference from breaking a whole sentence.
try($score / $count, 0)→ 0 — When $count is 0try($pc.nickname, "Friend")→ "Friend" — Fall back when the nickname field is missing
A random number between 0 and 1
Returns a fresh random number each time it's evaluated. Useful for percent-chance tests when combined with comparisons.
if(random() < 0.5, "heads", "tails")→ either value — A 50/50 coin flip
Roll dice — the spelled-out form of 3d6 / d20
Roll count dice, each with sides sides, and add them up. The friendly, named version of the dice shorthand: dice_roll(6) is the same as 1d6, and dice_roll(6, 3) is the same as 3d6. Both arguments are optional — sides defaults to 6 and count to 1 — so a bare dice_roll() rolls one six-sided die.
dice_roll(20)→ a number from 1 to 20 — One twenty-sided die (the same as d20)$health + dice_roll(6, 3)→ health plus 3–18 — Add three six-sided dice to a value
A random whole number between two values (both ends included)
Returns a fresh random whole number from from up to to, with both ends possible — so random_between(1, 6) is a six-sided die. The first number must not be greater than the second.
random_between(1, 6)→ a whole number from 1 to 6 — A single dice roll
Make the random results that follow repeatable (a testing aid)
Sets the starting point for every random result that comes after it — random(), dice, random_between(), .shuffle() and friends — so a run can be reproduced exactly. It exists primarily for testing: put seed(42) at the top of a script in the playground and every run gives the same rolls. Take it out for real play, or the game will roll the same numbers every time.
seed(42)→ "" — Random results after this point are now repeatable
A list of whole numbers, 1 up to count
Makes the list 1, 2, … up to count. Most often used to repeat something a set number of times with for each $i in range(3). To start somewhere other than 1, see range(from, to).
range(3)→ 1, 2, 3 — One up to three
A list of whole numbers, from…to (both ends included)
Makes the list of consecutive whole numbers from one value to another, INCLUDING both ends — range(2, 5) is 2, 3, 4, 5.
range(2, 5)→ 2, 3, 4, 5 — A from–to range, both ends included
Make a list from the given values (empty if you give none)
Builds a list out of the values you pass. list() is an empty list to fill in later (add to it in a script with $x.append(...)); the values can be mixed, like list("gold", 42, "gem").
list("sword", "shield", "potion")→ sword, shield, potion — A quick fixed list of three itemslist("gold", 42, "gem")→ gold, 42, gem — Values can mix text and numbers
A new empty record — a value with named fields — to fill in
Makes an empty record you then fill in by assigning named fields. In a script: set $hero = record() then $hero.name = "Steven". Handy for building a new entry before adding it to a list with $list.append(...).
record()→ an empty record — Start empty, then add fields by assignment
The current real-world time, in UTC
Returns the current real-world time as an ISO date string in UTC (Universal Time). Pair with .format_date(...) to display it. Returns the same shape of string as a date stored in a tracked item, so all the usual date tokens work.
now_utc().format_date("day monthname year")→ "13 May 2026" — Today's date in UTCnow_utc().format_date("hour2:minute2 AMPM")→ "02:35 PM" — Current UTC time on the 12-hour clock
The current real-world time, in the player's local zone
Returns the current real-world time in the player's local timezone (captured from their browser). Pair with .format_date(...) to display it. If the player's timezone isn't known to the server, falls back to UTC.
now_local().format_date("weekday, day monthname year")→ "Wednesday, 13 May 2026" — Today's date in the player's zonenow_local().format_date("hour24:minute2")→ "15:35" — Current local time on the 24-hour clock
Sine of x (radians)
Standard trigonometric sine. The angle is in radians, so a quarter turn is 1.5708 (π/2) and a full turn is 6.2832 (2π). Also available as a chain form: $angle.sin().
sin(1.5708)→ 1 — A quarter turn (π/2 radians)
Cosine of x (radians)
Standard trigonometric cosine. The angle is in radians, so a half turn is 3.1416 (π) and a full turn is 6.2832 (2π). Also available as a chain form: $angle.cos().
cos(3.1416)→ -1 — A half turn (π radians)
Tangent of x (radians)
Standard trigonometric tangent. The angle is in radians. Also available as a chain form: $angle.tan().
tan(0.7854)→ 1 — An eighth turn (π/4 radians)
e raised to the power of x
Returns the mathematical constant e (~2.718) raised to x. Mostly useful for advanced numeric formulas. Also available as a chain form: $x.exp().
exp(1)→ 2.71828…
Raise x to the power of n
Returns x raised to the power of n. Equivalent to the ^ operator — power(4, 2) and 4^2 both give 16. Also available as a chain form: $x.power(n).
power(4, 2)→ 16power(2, 10)→ 1024
The positive version of a number
Strip a minus sign from a number. Useful when you only care about the size of a change, not its direction. Also available as a chain form: $change.abs().
abs(-5)→ 5
Round a number down to the nearest whole number
Round towards negative infinity. 3.9 becomes 3; -1.2 becomes -2. Also available as a chain form: $value.round_down().
round_down(3.9)→ 3round_down(-1.2)→ -2
Round a number down to N decimal places
Round towards negative infinity at the given decimal place. round_down(3.78, 1) keeps one decimal and gives 3.7.
round_down(3.78, 1)→ 3.7round_down(123.456, 2)→ 123.45
Round a number up to the nearest whole number
Round towards positive infinity. 3.1 becomes 4; -1.8 becomes -1. Also available as a chain form: $value.round_up().
round_up(3.1)→ 4round_up(-1.8)→ -1
Round a number up to N decimal places
Round towards positive infinity at the given decimal place. round_up(3.12, 1) keeps one decimal and gives 3.2.
round_up(3.12, 1)→ 3.2round_up(123.451, 2)→ 123.46
Strings
Split text into single characters
Break a piece of text into a list of one-character pieces, including spaces and punctuation.
"hello".characters()→ ["h", "e", "l", "l", "o"]
True if the text ends with this
Check whether a piece of text ends with the given suffix. Returns "true" or "false". Case-sensitive.
$name.ends_with("III")→ "true" if the name ends with "III" — Useful for testing royal numerals
Split text into a list of lines
Break text on newline characters into a list of lines.
$letter.lines().count()→ how many lines the letter has
Convert text to lowercase
Make every letter lowercase. Useful before comparisons that should ignore case.
"HELLO".lower()→ "hello"
Replace one piece of text with another
Find every occurrence of old in the text and swap it for new. Case-sensitive.
$desc.replace("dragon", "wyrm")→ the description with dragon→wyrm
Split text on a separator
Cut the text into pieces wherever the separator appears, and return them as a list.
"a, b, c".split(", ")→ ["a", "b", "c"]
True if the text starts with this
Check whether a piece of text begins with the given prefix. Returns "true" or "false". Case-sensitive.
$name.starts_with("Sir")→ true or false
Capitalise The First Letter Of Each Word
Capitalise the first letter of each word, lowercase the rest.
"hello world".title_case()→ "Hello World"
Remove leading and trailing whitespace
Strip spaces, tabs and newlines from both ends of the text. Inner whitespace is left alone.
" hello ".trim()→ "hello"
Limit text to N characters
If the text is longer than N, cut it down to N characters. Texts shorter than N are returned unchanged. Nothing is appended unless you pass a suffix — see truncate(length, suffix).
$summary.truncate(20)→ the first 20 characters — Useful for previews
Limit to N characters and append a suffix
Like truncate(length), but append the suffix (e.g. "…") when the text was actually shortened. Short text is returned unchanged, with no suffix added.
$summary.truncate(20, "…")→ first 20 chars + "…" — Useful for previews
Convert text to UPPERCASE
Make every letter uppercase. Pairs nicely with markdown like .bold() for emphasis.
"alert".upper()→ "ALERT"
Split text into a list of words
Break text into words on word boundaries — punctuation is dropped, hyphens split words, apostrophes stay put inside contractions.
"hello, world!".words()→ ["hello", "world"]
Numbers
The positive version of the value
Strip a minus sign from the value. (-5).abs() becomes 5.
$change.abs()→ 5 if change is -5
Keep a number between two bounds
Clamp the value: anything below min becomes min, anything above max becomes max, otherwise return the value unchanged.
$health.constrain(0, 100)→ 100 if health is 150 — Stop overhealing
Cosine of the value (radians)
Standard trigonometric cosine. The value is in radians.
$angle.cos()→ -1.0 if angle is 3.1416
e raised to the power of the value
The mathematical constant e (~2.718) raised to the value.
$x.exp()→ 2.71828… if x is 1
Format a number with thousands commas
Insert commas every three digits, no decimals.
$gold.format_number()→ "1,234"
Format with thousands commas and N decimal places
Insert commas every three digits and keep N decimal places.
$gold.format_number(2)→ "1,234.56"
Format a number as a percentage
Multiply by 100 and add a percent sign, no decimals. 0.85 → "85%".
$ratio.format_percent()→ "85%"
Format a number as a percentage with N decimal places
Multiply by 100, add a percent sign, and round to the given number of decimal places. 0.8567 with 2 decimals → "85.67%".
$ratio.format_percent(2)→ "85.67%"
Add 1st, 2nd, 3rd, … to a number
Format a whole number with its ordinal suffix in English.
$rank.ordinal()→ "1st", "2nd", "3rd", "4th"
Raise the value to the power of n
Multiply the value by itself n times. (2).power(10) is 1024. Equivalent to the ^ operator: $x.power(2) is $x ^ 2.
$radius.power(2)→ 25 if radius is 5
Round to the nearest whole number
Standard rounding (.5 rounds up).
$price.round()→ 13 if price is 12.6
Round to N decimal places
Round to N digits after the decimal point.
$price.round(2)→ "12.99"
Round down to the previous whole number
Round towards negative infinity. 3.9 becomes 3; -1.2 becomes -2.
$value.round_down()→ 12 if value is 12.4
Round down to N decimal places
Round towards negative infinity at the given decimal place.
$value.round_down(1)→ 3.7 if value is 3.78
Round up to the next whole number
Round towards positive infinity. 3.1 becomes 4; -1.8 becomes -1.
$value.round_up()→ 13 if value is 12.4
Round up to N decimal places
Round towards positive infinity at the given decimal place.
$value.round_up(1)→ 3.2 if value is 3.12
Sine of the value (radians)
Standard trigonometric sine. The value is in radians, so a quarter turn is 1.5708 (π/2).
$angle.sin()→ 1.0 if angle is 1.5708
Tangent of the value (radians)
Standard trigonometric tangent. The value is in radians.
$angle.tan()→ 1.0 if angle is 0.7854
Lists
The item being updated, in a List Update effect
Only for the **List Update** effect on a YAML tracked item. As the update runs down the list, current() is the member being worked on — so current().hp is this member's hp. Use it in the **Target** to say which field to change, and in the **Value** to read this member's own fields (for example, heal everyone to full with Target current().hp and Value current().max_hp). It has no meaning anywhere else — the Playground, conditions, or Path Update — and reports an error if used there.
current().hp→ 23 — This member's hp — name it in the Target, or read it in the Value.current().max_hp→ 50 — This member's maximum hp — e.g. the Value when healing the party to full.
Every child item
Return all the child items. The same as .where() with a condition that's always true, but it moves the content to the children rather than a property of the current context — which is what lets $party.all().name reach each member's name field.
$party.all().name→ ["Gandalf", "Frodo", "Aragorn"] — All party members' names
True if at least one item matches
Test each item against the condition; return "true" if at least one matches, otherwise "false". The condition works just like the one in .where(): refer to fields directly and use operators such as =, <, >, contains, in, and and/or/not.
$party.any(name = "Gandalf")→ "true" — Is Gandalf in the party?$inventory.any(weight > 5)→ true or false — Any heavy items?
The average of the numbers in a list
Add up every value and divide by the count. Works on a list of plain numbers; for a list of objects, use .average(field).
$rolls.average()→ "4.5" — Average of [3, 4, 5, 6]
The average of one field across all items
Pick out a single field on every item and average those numbers. Equivalent to .all().{field}.average() but shorter.
$party.average(level)→ "13.33" — Average party level
Format the list as bullet points
Join the items into one string, each on its own line and prefixed with a dash. Pairs nicely with .format_each() to customise each row.
$skills.bulleted_list()→ "- swordsmanship\n- stealth\n- perception"
True if the list contains this value
Check whether the value is in the list. Returns "true" or "false". On a map, this checks the keys.
$skills.contains("archery")→ true or false — Has the player learned archery?
True if the collection contains this value (the reverse of contains)
The mirror image of .contains() — check whether THIS value is in the given collection, reading subject-first. $skill.in($unlocked) is the same as $unlocked.contains($skill). Works on lists (element), maps (key), and text (substring). Returns "true" or "false".
"Frodo".in($names)→ true or false — Is Frodo one of the names?
How many items are in the list
Return the number of items in the list (or entries in a map).
$party.count()→ "3"
Every nested item, flattened out
Walk a tree of nested objects and produce one big flat list of all the inner objects. Useful for skill trees, dialogue trees, or anything else with branching structure.
$skill_tree.descendants().name→ list of every skill name in the tree
True if every item matches
Test each item; return "true" only if all of them match. Returns "true" for an empty list (nothing fails). The condition works just like the one in .where(): refer to fields directly and use operators such as =, <, >, contains, in, and and/or/not.
$party.every(level > 0)→ "true" — Is every party member at least level 1?
The first item in the list
Return item number 1. Same as .item(1) but shorter to read.
$party.first().name→ "Gandalf"
The first n items
Return up to the first n items as a new list. If the list is shorter than n, you get the whole list back.
$party.first(2).all().name→ ["Gandalf", "Frodo"]
Combine nested lists into one
Take a list of lists and flatten one level so it becomes a single list. Doesn't dig deeper than one level. You rarely need it after a selection — a field read over .where(...)/.all() already gives one flat list of all the values.
$teams.flatten()→ ["Gandalf", "Frodo", "Aragorn"] — When $teams is [["Gandalf", "Frodo"], ["Aragorn"]]
Apply a template to each item
Render a template string once per item, then return a list of the rendered strings. Inside the template, fields of the item are available directly by name (case-sensitive — {XP} and {xp} are different), and you can use index() for the current item's position (1-based). A bare name reaches only the item's own fields; to read an outer tracked item, prefix it with $ (e.g. {$gold}).
$party.format_each("{name} the {class}")→ ["Gandalf the wizard", …]$party.format_each("{index()}. {name}").join("\n")→ "1. Gandalf\n2. Frodo\n3. Aragorn" — Combine with .join() for a numbered roster
The nth item in the list (1-based)
Pick out a single item by its position. Position 1 is the first item, position 2 the second, and so on. On a map, .item(...) takes a key instead — see .item(key) under Maps.
$party.item(2).name→ "Frodo"
Glue items together with a separator
Combine every item into one piece of text, separated by the given string.
$names.join(", ")→ "Gandalf, Frodo, Aragorn"
Glue items, with a different last separator
Like .join(sep) but use a different separator before the last item — handy for natural-language lists.
$names.join(", ", " and ")→ "Gandalf, Frodo and Aragorn"
The last item in the list
Return the final item. Same as .item(N) where N is the count.
$party.last().name→ "Aragorn"
The last n items
Return the final n items as a new list.
$rolls.last(3)→ the most recent three rolls
The largest value in the list
Return the highest value in a list of plain numbers (or the one that sorts last alphabetically in a list of strings). For the largest *value of a field* across a list of objects, use .max(field). For the *item* with the largest field value, use .max_by(field).
$rolls.max()→ "6"
The largest value of a field across items
Evaluate field (or any per-item expression) on each item and return the largest result as a number. Returns a *value*, not an item — use .max_by(field) if you want the item itself.
$party.max(level)→ "20" — Highest level value in the party$party.max(level + 5)→ "25" — Per-item arithmetic also supported
The item with the largest field value
Evaluate field on each item and return the *item itself* whose field value is largest. Different from .max(field), which returns just the value. Chain .max_by(field).<field> to get back to the value if needed. Always a single item — if several tie for the largest value, you get the first of them in list order. To act on ALL tied items, filter instead: $party.where(level = $party.max(level)).
$party.max_by(level).name→ "Gandalf" — Name of the highest-level party member$inventory.max_by(value)→ an item object — The most valuable item in the inventory
The smallest value in the list
Return the lowest value in a list of plain numbers (or the one that sorts first alphabetically in a list of strings). For the smallest *value of a field*, use .min(field). For the *item* with the smallest field value, use .min_by(field).
$rolls.min()→ "1"
The smallest value of a field across items
Evaluate field (or any per-item expression) on each item and return the smallest result as a number. Returns a *value*, not an item — use .min_by(field) if you want the item itself.
$party.min(level)→ "5" — Lowest level value in the party
The item with the smallest field value
Evaluate field on each item and return the *item itself* whose field value is smallest. Different from .min(field), which returns just the value. Always a single item — if several tie for the smallest value, you get the first of them in list order. To act on ALL tied items, filter instead: $party.where(hp = $party.min(hp)).
$party.min_by(level).name→ "Frodo" — Name of the lowest-level party member
True if no items match
Test each item; return "true" only if none of them match. Equivalent to .any(condition).not(). The condition works just like the one in .where(): refer to fields directly and use operators such as =, <, >, contains, in, and and/or/not.
$party.none(class = "necromancer")→ "true" — No necromancers here
Format the list as a numbered list
Join the items into one string, each on its own line, numbered from 1.
$objectives.numbered_list()→ "1. Find the ring\n2. Reach Mount Doom"
Join items with newlines, no prefix
Like .bulleted_list() and .numbered_list() but with nothing in front of each line — just the items separated by newlines.
$names.plain_list()→ "Gandalf\nFrodo\nAragorn"
One random item from the list
Pick one item at random. Each call may return a different item. Different from the top-level random() which gives a number.
$insults.random()→ one of the insults
n random items from the list
Pick n items at random, without repeats. If n is bigger than the list, you get the whole list shuffled.
$loot_table.random(3)→ three random items
Reverse the order of the list
Return the items in opposite order. Often paired with .sort() for descending order, though .sort_by(field, desc) is clearer when sorting by a field.
$names.sort().reverse()→ sorted Z–A
Randomise the order of the list
Return the items in a random order. Useful for picking a rotating subset, or for pseudo-random adventure flavour.
$encounters.shuffle().first(3)→ three random encounters in random order
Sort the list (ascending)
Sort numerically or alphabetically, smallest/A first. For objects with multiple fields, use .sort_by(field) instead. Capital letters sort before lowercase.
$names.sort()→ ["Aragorn", "Frodo", "Gandalf"]
Sort the list by one field (ascending)
Sort a list of objects by the value of one field. The field name isn't quoted — just write it like a path. Items missing the field sort to the end.
$party.sort_by(level).all().name→ ["Frodo", "Aragorn", "Gandalf"] — Lowest level first
Sort by one field (descending with `desc`)
Same as .sort_by(field) but flip the order with the bare word desc. Use the bare word asc for ascending if you like being explicit.
$party.sort_by(level, desc).all().name→ ["Gandalf", "Aragorn", "Frodo"] — Highest level first
Add up all the numbers
Total up the items in a numeric list. For a list of objects, use .sum(field).
$rolls.sum()→ "21"
Add up one field across all items
Total a single field across every item. You can also use a short calculation in place of a field name (level + bonus).
$party.sum(level)→ "40"$party.sum(level + bonus)→ "45" — Sum a calculated value
A tie-breaker for sort_by (ascending)
Comes after .sort_by(...) (or another .then_by(...)) and breaks ties using a second field.
$items.sort_by(type).then_by(weight)→ items grouped by type, then sorted by weight
A tie-breaker for sort_by (descending)
Same as .then_by(field) with the order flipped.
$items.sort_by(type).then_by(weight, desc)→ items grouped by type, heaviest first within each group
Join two lists into one
A new list holding this list's items followed by the other's. Duplicates are KEPT (an inventory with three potions has three potions) — chain .unique() when you want each value once. The other set questions are filters: .where(value() in $other) keeps the items both lists share, and .where(value() not in $other) keeps the items the other list doesn't have.
$tags.union($new_tags)→ ["forest", "river", "forest", "ruined", "river", "cursed", "river"]$tags.union($new_tags).unique()→ ["forest", "river", "ruined", "cursed"] — Set-style union — each value once$tags.where(value() not in $new_tags)→ ["forest", "forest", "ruined"] — The difference is a filter, not a function
Remove duplicate items
Return a new list with each value appearing only once. Order is preserved (first occurrence wins).
$tags.unique()→ ["forest", "river", "ruined"]
Keep only the items that match a test
Test each item against the condition; keep the ones that match, drop the rest. Inside the condition, fields are available directly, and you can use =, <, >, contains, in, etc.
$party.where(level > 10).name→ high-level party members$inventory.where(type = "weapon")→ just the weapons$skills.where(name contains "fire")→ fire-related skills$tags.where(value() != "forest")→ ["river", "ruined", "river"] — For a list of plain values, value() is the item itself
Maps
True if the path leads somewhere
Check whether the chain you've just walked through actually points at something — useful for guarding against missing fields or empty maps. Returns "true" or "false".
$pc.nickname.exists()→ true or false — Has a nickname been set?
A map entry by its key — with an optional fallback
Pick a map entry by key. The key can be a quoted string or any expression ($favourite), which is how you reach an entry whose name is stored in data. A second value is a READ fallback, returned when the key isn't there — the safe-tally idiom for keys that may not exist yet. In a script, .item(key) also works as an assignment target (the fallback belongs on the right-hand side: $rep.item($f) = $rep.item($f, 0) + 5). For a key you already know, a quoted field is usually tidier — $names_list."My Name" reads the same entry as .item("My Name"). Keep .item() for a key held in a variable or when you want a fallback.
$inventory.item("sword").value→ 100$inventory.item($favourite).quantity→ 3 — The key comes from another tracked item$reputation.item("orcs", 0)→ 0 — No orcs entry yet — the fallback is returned
The key of a single map entry
When you've narrowed a map down to one entry (e.g. with .first() or .where(...)), this gives you that entry's key. Inside .format_each() it gives the current entry's key.
$inventory.first().key()→ "sword"
A list of the map's keys
Get every key from a map (or the keys of the map entries that survived a .where() filter).
$inventory.keys()→ ["sword", "shield", "potion"]
The value of a single map entry, or the current item itself
Mirror of .key() — when you've narrowed a map to one entry, this returns its value. Inside .where() and .format_each() it is the current entry's value — and when the items are plain values rather than blocks of fields, it is the item itself. That's how you test a plain item in a filter, since bare names only reach fields.
$inventory.first().value()→ the sword object$names.where(value().contains("o"))→ ["Frodo", "Aragorn"] — Filter a list of plain values by the item itself$names.format_each("Hail, {value()}!")→ ["Hail, Gandalf!", …] — Reference the plain item inside a template
A list of the map's values
Get every value from a map, dropping the keys.
$inventory.values().count()→ how many items in the inventory
Scripts
Add a value to the end of a list (script statement)
In a "Run a script" effect, add a value to the end of a list. Given a whole list (or a .where() selection), each of its values is appended, so the result stays flat. Script only — expressions can't change values, so this never appears inside <<…>>.
$quiver.append("arrow")→ the quiver gains an arrow$satchel.append($found_pouch)→ every pouch item joins the satchel, one flat list
Remove from a list or map (script statement)
In a "Run a script" effect: on a MAP, removes the entry with the given key; on a LIST, removes every item equal to the value. With NO value, .remove() closes a selection — select, then remove: $party.where(hp <= 0).remove() deletes exactly the selected slots, and on a map .where(...) selects entries by key() / value() conditions. Script only.
$inventory.remove("sword")→ the sword entry is gone$tags.remove("forest")→ every "forest" tag is gone$party.where(level < 10).remove()→ the low-level members are gone — Select, then remove — reads in execution order$reputation.where(value() = 0).remove()→ every zero-score faction is gone
Leave a note for the world designer (script statement)
Record a message for YOU, the designer — players never see it. Messages appear in the playground's Log section and in World Debug's PawScript Log. A quoted message works like format_text(...) ({…} slots evaluate); any other value logs as-is. The optional level is "info" (default) or "error". Messages logged before a failed run still arrive — they're your key diagnostic exactly then.
log("gold is {$gold}")→ logs "gold is 100"log("stock ran dry", "error")→ logged with error styling
The current item's position (1-based)
On a for each loop variable ($m.index()) — and inside .format_each(...) templates — the position of the current item, starting at 1. Handy for ranks and numbered lines.
$party.format_each("{index()}. {name}")→ ["1. Gandalf", "2. Frodo", "3. Aragorn"]for each $m in $party $m.rank = $m.index()→ each member gains their marching-order rank
True for the first item
On a for each loop variable — and inside .format_each(...) templates — "true" when the current item is the first one.
$party.format_each("{name}: {is_first()}")→ ["Gandalf: true", "Frodo: false", "Aragorn: false"]
True for the last item
On a for each loop variable — and inside .format_each(...) templates — "true" when the current item is the last one, e.g. to treat the final entry differently.
$party.format_each("{name}: {is_last()}")→ ["Gandalf: false", "Frodo: false", "Aragorn: true"]
Markdown
Wrap the text so it renders **bold**
Surround the text with markdown bold markers. The renderer will display it as bold; outside markdown contexts you'll see the asterisks.
$name.bold()→ "**Aragorn**"
Wrap the text so it renders as `code`
Surround the text with markdown backticks for inline code.
$skill.code()→ "`stealth`"
Wrap the text so it renders *italic*
Surround the text with markdown italic markers.
$location.italic()→ "*Bree*"
Wrap the text so it renders ~~struck through~~
Surround the text with markdown strikethrough markers — useful for crossed-out items in a quest log.
$objective.strikethrough()→ "~~Find the ring~~"
Other
Format a date with English-word tokens
Format a date string using friendly tokens like day, monthname and year. Anything in the pattern that isn't a token is kept as-is, so spaces, commas, slashes and dashes all work as separators.
The tokens (showing what each produces for Wednesday 8 April 2026, 2:07:08 PM):
- day / day2 — day of month (8 / 08) - month / month2 — month number (4 / 04) - monthname / monthname3 — month name (April / Apr) - year / year2 — year (2026 / 26) - weekday / weekday3 / weekday2 / weekday1 — day of week (Wednesday / Wed / We / W) - hour / hour2 — 12-hour clock (2 / 02), pair with an AM/PM marker - hour24 — 24-hour clock, 2 digits (14) - minute / minute2 — minute (7 / 07) - second / second2 — second (8 / 08) - AMPM / ampm — AM/PM marker, upper- or lower-case (PM / pm)
$date.format_date("day monthname year")→ "5 April 2026"$date.format_date("weekday, day monthname3 year")→ "Wednesday, 5 Apr 2026"$date.format_date("hour2:minute2 AMPM")→ "02:30 PM" — hour/hour2 are the 12-hour clock$date.format_date("hour24:minute2")→ "14:30" — hour24 is the 24-hour clock$date.format_date("day2/month2/year2")→ "05/04/26" — Slashes are kept as-is
Flip true to false (and vice versa)
Logical NOT. Useful after a test that returns "true" / "false" when you want the opposite.
$skills.contains("archery").not()→ "true" — When archery is NOT in the list