Vellum · Reference

Writing a dialogue script

The format you hand to vellum quest draft, which turns it into the quest description Vellum builds from. Nothing here is convention: what this page says is what the reader does, and there is a test for every rule on it.

It is not a tree

This is the one thing that decides everything else. Skyrim dialogue is not a tree — it is a flat list of topics, and branching exists because one answer unlocks other topics.

So the natural way to write it is the one that cannot be converted:

PLAYER: Who are you? └─ Hauriel: Nobody you need to worry about. └─ PLAYER: I insist. └─ Hauriel: Then insist sitting down.

Lovely to read, and impossible to convert without inventing. There are three topics in there and not one of them has a name — whoever gives them ids ends up deciding your structure for you.

Write it flat, with names. Indentation does not survive the conversion; ids do.

# Who top PLAYER: Who are you? > Nobody you need to worry about. then: Insist, Leave

One block per topic

# <Id> top PLAYER: <the line that appears in the list> > <what she says back> if: `<condition>` then: <Id>, <Id> end

The word at the end of the heading says what kind of topic it is.

HeadingWhat it means
topAppears in the list on its own, as soon as its conditions allow.
follows <Id>Only reachable because another response named it in then:. The id is the topic where the branch starts.
IDLEAn ambient line — she says it herself. No prompt, no branch, never in a list.
HELO / GBYEThe greeting and the goodbye. Also hers to say.

The fields of a response

All optional except the text itself. They live at two spaces of indentation, under the > they belong to.

FieldWhat it does
if:One condition, in backticks. Several lines add up with AND.
or:Joins the condition above it with OR instead of AND.
then:Ids of the topics that become available after this line.
endCloses the conversation after this line.
SayOnceSaid once, and never again.
prompt:Replaces the player's line, for this response only.
emotion:Happy 55 — a name and a value.
set:set: Yae_Isekai = 1. The line sets the global, and the global is what opens other conversations.
rapport:+1 — calls the friendship system declared in the header.
why:A comment. It goes nowhere — it is the reason the condition is what it is, and it is half of a good script.

Conditions for a whole topic

An all: line, outside any response, applies to every line from there to the end of that topic — and only that topic.

# CivilWar IDLE all: `GetRandomPercent <= 15` all: `IsSneaking == 0 @player`

Three rules that save you

1  ·  The order of the responses is the logic

The game plays the first response whose conditions pass. Not the best one, not the most specific — the first in the list. So the specific goes on top, and the generic goes last with no conditions at all, as a net.

> We've been over this. Nothing's changed. if: `GetStage HAU_Hauriel >= 40` > Hauriel. It won't mean anything to you.

The other way round, the generic catches everything and the line below it is never heard — and in game that is not an error, it is a character who repeats herself.

2  ·  Conditions live on the response, not the topic

A topic is just the box. Each line inside it has its own conditions. If you want the player's line to disappear from the list in some circumstance, put conditions on every response — when none of them pass, the topic does not appear.

3  ·  Two lines means two recordings

Every > is one audio file. Three phrasings of the same idea cost three recordings — sometimes worth it, so she does not repeat herself, and sometimes not. Decide that while writing, not while recording.

Writing the conditions

While you are drafting, a condition can be plain prose — that is how you write a script before you know the function names. What has to be explicit is about whom and about what.

Write thisBecomes
stage >= 40GetStage self >= 40
the player has 50 goldGetItemCount Gold001 >= 50, run on the player
she is following meGetVMQuestVariable ::IsFollowing_var == 1
we are in a cityLocationHasKeyword LocTypeCity == 1
we are not in a dungeonLocationHasKeyword LocTypeDungeon == 0
she likes me, at leastGetRelationshipRank >= 3
it is daytimeGetCurrentTime > 6 and < 20

But it has to stop being prose before quest draft, which refuses what it does not understand. That is deliberate: a condition that reads beautifully and does not exist saves perfectly well and then fails in game, silently.

A complete example

This passes vellum quest draft exactly as it stands. The > sits in column zero.

# Hauriel — who she is ```quest id: HAU_Who name: Hauriel - Who prefix: HAU_Who priority: 50 speaker: HAU_Hauriel masters: Skyrim.esm, HAU_Hauriel.esp ``` # Who top PLAYER: So who are you, really? > I told you once. It didn't improve with repetition. if: `GetStage HAU_Hauriel >= 40` > Hauriel. The rest stays where it is. prompt: Who are you? then: Insist, Leave # Insist follows Who PLAYER: That isn't an answer. > No. But it's the one you have. if: `GetRelationshipRank HAU_Hauriel <= 0` end > Fine. Sit down. This takes a while. set: HAU_ToldAll = 1 then: History why: this is the step that opens the history. The global stays, and the conversation below reads it — the line sets it, not the quest # Leave follows Who PLAYER: Fair enough. Leave it. > Thank you. end

Two things the reader adds on its own, and both matter:

  • The topic is marked random, because it has more than one response. Without that the engine plays the first one whose conditions pass and the other never comes out.
  • The global appears in the quest's list purely because someone wrote set:.

What the reader refuses

Four of them, and every one is a bug that actually shipped — none of which raised an error before the reader started refusing them.

  1. An all: does not carry into the next topic. Letting it carry glued a GetRandomPercent <= 15 onto a conversation: the question only appeared one time in six.
  2. An if: replaces an all: only for the same function and the same parameter. Matching on the function alone once let a GetGlobalValue FriendShip silently erase a GetGlobalValue PlayerIsWerewolf — and the werewolf line went out to people who had never been one. Every replacement is printed now.
  3. An indented line that opens with word: and is not a real field is an error. It used to go straight into the spoken text, and the actor read it out. A genuine continuation — one line of speech wrapped over two — still works.
  4. The lines are counted. The reader counts the > lines in the file and compares with how many came out. When a heading does not parse, its lines fall into the topic above or out of everything, and the only sign is a lower total.

What does not help

  • Flowing prose. "She says something evasive and gives in if you push" cannot be converted, only rewritten — and then the character is the rewriter's.
  • An indented tree with no ids. See the top of this page.
  • All of it at once. One branch converts, checks and tests in game. Thirty topics in one go means finding the mistake after everything is recorded.
  • Implicit conditions. "Only if they're friends already" is fine; "only at the right moment" is not.

Ids

  • Short, and no spaces. They end up in EditorIDs and in fragment script names, and those have length limits — a long id inside a long quest name overruns, and then the tool has to cut it.
  • Unique within the quest.
  • They never appear in game, so they can be in any language. What does appear is the PLAYER: line and the > line — write those in the language your mod ships in.

Scenes are different

A scene has no player choice: it is a script that runs on its own, in phases, and it is how she talks to another character, or to herself, while doing something.

SCENE <Id> — <what is happening> starts: <what fires it> ends: <the condition that finishes it> phase 1 HAURIEL: <line> does: <package, animation, whatever> phase 2 HAURIEL: <line>
  • One phase, one line. Measured across the reference mods: 1518 of 1519 dialogue actions in Serana Dialogue Add-On and in Ashe fit in a single phase. Do not pile them up.
  • Always say the ends:. A scene with no end condition runs until something outside stops it. Vanilla Skyrim's own AventusSacramentScene has the condition written and never meets it — the code that would set it true is commented out. Copy the shape, not the wiring.

Stages, items and packages are not written in the script at all — those live in the quest's .json. A script is about lines, conditions, and the two things a line does to the world: set: and rapport:.