Pastura scenario format reference
This page is the complete reference for the YAML format Pastura scenarios use.
It is written for both people and language models. If you are asking an LLM to
draft a scenario, you can point it at the raw Markdown version of this page at
https://pastura.app/docs/scenario/format.md.
A scenario is a single YAML file. Pastura runs it by having a local LLM play each agent, round by round, following the phases you declare. Nothing in a scenario is sent to a server.
Top-level structure
id: unique_snake_case_id # required. Stable identifier, snake_case
language: en # required. Authoring language: `ja` or `en`
name: My Scenario # required. Display title
description: One-line summary # required
agents: 5 # required. Number of agents, 2 to 10
rounds: 3 # required. Number of rounds, 1 to 30
context: | # required. Shared briefing every agent sees
You are contestants in a game...
personas: # required. One entry per agent (length == agents)
- name: Alex
description: A calm strategist who plans several moves ahead.
secret: You already sold the family house. # optional. Hidden from the others
- name: Mia
description: An optimist who trusts people by default.
phases: # required. The ordered list of what happens
- type: speak_all
prompt: What do you say to the group?
output:
statement: string
inner_thought: string
Write every human-facing string (name, description, context, each
prompt, each template, and every persona name / description / secret)
in the language named by language. That value drives how the engine prompts
the model.
A persona may also carry an optional secret, its hidden agenda. The engine
injects it into that agent’s own prompt only, so the other agents never learn
it and cannot react to it. The agent is told to keep it out of anything the
others can hear, while its inner_thought may name it freely. You (the viewer)
can reveal it from the persona sheet during a run, which is what makes the
dramatic irony readable. Use secret when you want a public face and a private
motive to differ. Keep the whole persona in description when you don’t.
Optional top-level keys:
simulation_languagesets the language the agents speak when it differs from the authoring language. Omit it to speak inlanguage.log_windowsets how many recent conversation entries each prompt carries. It must be at least the agent count when aspeak_eachphase is present, or earlier speakers in the same round drop out of later prompts.
Do not add a top-level min_engine_version key. It is not part of the scenario
YAML schema, and a bare integer value fails to load. Compatibility is handled
by the gallery index, not by the scenario file.
Phases
Phases run top to bottom, once per round. There are two families.
LLM phases run one model inference per agent (or one per round for
narrate). Each declares an output block naming the fields the model fills
in.
| Phase | What it does | Primary field | Private-thought field |
|---|---|---|---|
speak_all |
Every agent addresses the group at once | statement |
inner_thought |
speak_each |
Agents speak one at a time, each seeing the last | statement |
inner_thought |
vote |
Each agent names another agent | vote |
reason |
choose |
Each agent picks from declared options |
action |
inner_thought |
reflect |
Each agent privately updates a short note | note |
(none) |
whisper |
Pairs of agents exchange a private line | statement |
inner_thought |
narrate |
A commentator narrates the round highlight | (engine-fixed) | (none) |
Code phases run deterministically with no model call, so they declare no
output block.
| Phase | What it does |
|---|---|
score_calc |
Applies a built-in scoring logic to update scores |
assign |
Distributes values from a source list to agents |
eliminate |
Removes the most-voted agent from later rounds |
summarize |
Emits a recap line from a template string |
conditional |
Runs one branch of sub-phases based on an if expression |
event_inject |
Injects a random event string into the run state |
relationship_update |
Updates an affinity matrix from vote and choose history |
Output fields and canonical names
The output block names are not free-form. Each LLM phase has one canonical
primary field and, for most, one canonical private-thought field, shown in the
table above. Use those exact names. A scenario that saves with a different name
(for example message instead of statement) is rejected when you commit it in
the editor.
- type: vote
prompt: Who do you suspect, and why?
output:
vote: string # canonical primary for `vote`
reason: string # canonical private-thought for `vote`
The private-thought field is display-only. Other agents never see it, so it is where a model can reason honestly before it speaks.
Two rules on the names themselves:
- Field names must be ASCII letters, digits, and underscores, starting with a letter. A non-ASCII field name can crash on-device inference. Field values may be any language.
narrateis special. Its output shape is fixed by the engine, so anarratephase declares nooutputblock at all.
Scoring logics
A score_calc phase names one built-in logic:
| Logic | What it rewards |
|---|---|
prisoners_dilemma |
The cooperate / betray payoff matrix, per pairing |
vote_tally |
One point per vote received |
wordwolf_judge |
Whether the group voted out the odd-one-out |
event_reactive |
Agents whose last choose matched the injected event |
pairwise_payoff |
A payoff table you author in YAML, scored per pairing |
Each logic expects specific phases earlier in the round. See the pitfalls section below.
Phase field reference
Fields you can set on a phase, beyond type, prompt, and output:
options(forchoose): the list the action must come from. Without it, the action is unconstrained free text.pairing(forchooseand some scoring):round_robinpairs every agent with every other,individualgives each agent an independent decision.target(forassign):allgives every agent the same value,random_onegives a single random agent the value (used for the odd-one-out in word-wolf style games).source(forassignandevent_inject): the name of a top-level list key holding the values to distribute or inject. You add that key yourself (for examplewords:ortopics:) alongside the required top-level keys, and it must be non-empty.rounds(forspeak_eachandwhisper): how many sub-rounds of speaking happen within the phase.logic(forscore_calc): one of the scoring logics above.payoff(forscore_calcwithpairwise_payoff): a list of rows, each{ when: [action1, action2], points: [p1, p2] }. A pairing is matched againstwhenpositionally and awardspointsto the two agents. A pairing matching no row scores nothing.template(forsummarize): the recap string, which may contain{...}placeholders such as{scoreboard}or{current_round}.if,then,else(forconditional): a condition expression and the sub-phase lists for each branch.probability,as,no_repeat(forevent_inject): the chance of firing, the variable name the event is stored under (defaultcurrent_event), and whether to avoid repeating a previous event.narrator(fornarrate): the persona that delivers the commentary.moodis an optional private-thought output field you can add to any LLM phase to let a model carry emotional momentum between rounds.max_sentencescaps a spoken phase’s length. It only affects LLM phases.
Condition expressions
A conditional phase branches on an if expression. Expressions combine
comparisons with &&, ||, and parentheses.
- type: conditional
if: current_round == total_rounds && max_score >= 10
then:
- type: summarize
template: "Final round. {vote_winner} is ahead."
else:
- type: speak_all
prompt: The game continues.
output:
statement: string
inner_thought: string
Comparison operators are ==, !=, <, <=, >, >=. Variables you can
reference include current_round, total_rounds, max_score, min_score,
eliminated_count, active_count, vote_winner, and scores.<Name> for a
named agent. Scoring logics can expose extra scenario-specific variables. For
example, wordwolf_judge sets wolf_name to the agent holding the minority
word, which the Word Wolf example below uses in its final conditional.
Quote string values with double quotes. name == "Alex" compares against
the text Alex. A single-quoted 'Alex' is read as an undefined identifier, so
the comparison is always false and the branch silently never runs.
Common pitfalls
These are silent no-ops. The scenario loads, but a phase does nothing useful because a dependency is missing or misplaced. The in-app editor flags the blocking ones, but it is easier to get them right the first time.
eliminateneeds avoteearlier in the same round. Without it, there is no tally to eliminate from.prisoners_dilemmaneeds around_robinchoosephase before it, which is what creates the pairings it scores.pairwise_payoffneeds the sameround_robinchoosebefore it, plus apayofftable whosewhenrows cover thechooseoptions.wordwolf_judgeneeds both anassignwithtarget: random_one(to pick the odd-one-out) and avotebefore it.event_reactiveneeds anevent_injectbefore it, storing the event under a variable the scoring reads.assignneeds a non-emptysource. An empty list distributes nothing.- In a condition, compare text with double quotes, and make sure a bare word on
either side of
==is a real variable, not a persona name you meant to quote.
A complete example
The Word Wolf preset, with personas and prompt text trimmed for brevity:
id: word_wolf_en
language: en
name: Word Wolf
description: Players discuss a topic word, but one holds a different word.
agents: 5
rounds: 1
context: |
You are a contestant on the game show "Word Wolf".
Everyone has a topic word, but one player's word differs.
Never say the word itself. Describe concrete features that
evoke it, then vote to expose the minority.
words: # custom top-level list, referenced by source
- majority: apple
minority: orange
mid_game_announcements:
- "Host: Time is short. Narrow it down."
personas:
- name: Avery
description: A calm observer who leads with colour and shape.
- name: Riley
description: An outgoing talker who describes taste and texture.
# ...three more personas...
phases:
- type: assign # give one player the minority word
source: words
target: random_one
- type: speak_each
prompt: Describe your topic without ever naming it.
output:
statement: string
inner_thought: string
rounds: 2
- type: reflect
prompt: Who seems suspicious? Update your private note.
output:
note: string
- type: event_inject # maybe interrupt with a show announcement
source: mid_game_announcements
probability: 0.5
- type: conditional
if: 'current_event != ""'
then:
- type: summarize
template: "{current_event}"
- type: narrate
narrator: an enthusiastic play-by-play commentator
prompt: Call out the most suspicious player and the decisive mismatch.
max_sentences: 3
- type: vote
prompt: Vote for the minority whose clues are out of step.
output:
vote: string
reason: string
- type: eliminate
- type: score_calc
logic: wordwolf_judge
- type: conditional # branch on whether the group caught the wolf
if: "vote_winner == wolf_name"
then:
- type: summarize
template: "Wolf found. {wolf_name} was the minority. The majority wins."
else:
- type: summarize
template: "Wolf escapes. The minority was {wolf_name}. The votes went elsewhere."