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:
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.
The header block
The document has to say which quest it is. A fenced
quest block, right after the title. Without it the file
does not identify itself, and a format that only works alongside a table that does
not travel with the file is not a format.
| Key | What it is for |
|---|---|
id | Required. The quest's EditorID. |
prefix | What opens each topic's EditorID. It counts towards the voice filename, which cuts the topic at 15 characters. |
priority | Who wins when two quests want to speak through the same mouth. A generic pool belongs at the bottom. |
speaker | The NPC's EditorID. |
masters | Comma-separated. So are imports and globals. |
name, type,startGameEnabled,runOnce, plugin | The rest of the quest record. |
rapport | <formid> "<plugin.esp>" <Script>.<Method> — only needed if some line uses rapport:. |
One block per topic
The word at the end of the heading says what kind of topic it is.
| Heading | What it means |
|---|---|
top | Appears 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. |
IDLE | An ambient line — she says it herself. No prompt, no branch, never in a list. |
HELO / GBYE | The 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.
| Field | What 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. |
end | Closes the conversation after this line. |
SayOnce | Said 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.
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.
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 this | Becomes |
|---|---|
| stage >= 40 | GetStage self >= 40 |
| the player has 50 gold | GetItemCount Gold001 >= 50, run on the player |
| she is following me | GetVMQuestVariable ::IsFollowing_var == 1 |
| we are in a city | LocationHasKeyword LocTypeCity == 1 |
| we are not in a dungeon | LocationHasKeyword LocTypeDungeon == 0 |
| she likes me, at least | GetRelationshipRank >= 3 |
| it is daytime | GetCurrentTime > 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.
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.
-
An
all:does not carry into the next topic. Letting it carry glued aGetRandomPercent <= 15onto a conversation: the question only appeared one time in six. -
An
if:replaces anall:only for the same function and the same parameter. Matching on the function alone once let aGetGlobalValue FriendShipsilently erase aGetGlobalValue PlayerIsWerewolf— and the werewolf line went out to people who had never been one. Every replacement is printed now. -
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. -
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.
- 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 ownAventusSacramentScenehas 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:.