Skip to content

How-To: Author a Quest (manifest → stamp → dialogue → validate → overview)

last-verified: 2026-07-10 against 1c31ca4d0

This is the follow-along recipe for turning a quest design (paper, sheet, or CSV from the narrative designer) into working game content through the narrative pipeline. Reference docs: Tools/Narrative/SPEC_FORMAT.md (full manifest schema) and Documentation/08_Progression/01_Quest_System.md (how quests work at runtime).

The pipeline in one line:

manifest ──stamp──▶ tags + QuestDataAsset ──(you)──▶ dialogue graphs ──validate──▶ overview HTML

What you need before starting

  • The quest design: name, description, the story beats, objectives, rewards, compendium entries, and the dialogue (speakers, lines, branches, and where quest progress happens).
  • The editor closed for headless stamping, or open with the MCP bridge for live stamping.

Step 1 — Derive the tag graph

Quests are tag graphs (no checklist engine). From the design, list every moment the game must remember, and give each a tag under one root:

Design says Tag
the quest itself Quest.RedDesert (root — goes in quest.tag, NOT in tags[])
"after talking to Martha about the Red Bane" Quest.RedDesert.Discovery.RedBaneExplained
"when entering the Dead Mines" Quest.RedDesert.Objective.EnteredMines
an objective is done one tag per objective — this becomes its completionTag
quest complete Quest.RedDesert.Completeddo not author it; the stamp always creates <root>.Completed for you

Rules of thumb: Discovery.* for lore/knowledge beats, Objective.* for progress beats. Every compendium entry's unlock condition must map to a tag that something grants with an exact match — a child grant does NOT unlock a parent-tagged entry.

Step 2 — Write the manifest

Create Tools/Narrative/Quests/Quest_<Name>.quest.json. Copy an existing one as a template (Quest_Martha.quest.json is small; SPEC_FORMAT.md documents every field). Filling it from a designer sheet/CSV maps like this:

Sheet section Manifest section Notes
Quest name/description quest.name / quest.description asset: /Game/DataAssets/Quests/Quest_<Name> (created if missing)
Objectives objectives[] completionTag from Step 1; optional: false
Dialogue dialogues[] (paths only) line text is authored in Step 4, never in the manifest
Compendium / lore compendium[] entryId globally unique — prefix with quest name (RedDesert_Cinob); "unlocked when" prose → requiredTag; relatedQuestTag: null defaults to the root
Journal tile the entries live under compendium[].category + categories[] thematic/locational (Compendium.Category.RedDesert — "The Red Desert"), NOT one-tab-per-quest; declare display data (title, banner) in categories[], the stamp creates the display asset. Reuse an existing category when the lore belongs there
"The entry grows when the player later does X" deepen[] stage = entryId + exact requiredTag + new content (mode: append/replace); entry flips back to unread, no toast. Target may be another quest's entry
Items (rewards) rewards[] itemTemplate must resolve to a UItemManifestDataAsset (not a TPL_)
Every "when X happens" that is NOT in a dialogue granters[] declare verb + tag + via (item/map/blueprint) + source. This is how kill/location/pickup triggers pass validation before the content exists
Recurring lore keywords redText[] rendered red in compendium/dialogue text

Also add one tags[] entry (with a comment — it becomes the ini DevComment) for every tag from Step 1 except the root and .Completed.

Never write a _meta block by hand — it is tool-owned; a manifest without one stamps unconditionally on first run.

Step 3 — Stamp

Editor closed (headless):

& "C:\Program Files\Epic Games\UE_5.6\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" `
  "C:\Projects\Project-Eternal\ProjectEternal.uproject" -run=pythonscript `
  -script="Tools/Narrative/quest_stamp.py stamp Tools/Narrative/Quests/Quest_<Name>.quest.json"

Editor open: run the same quest_stamp.py via MCP system_control execute_python.

Always read the report at Saved/Narrative/last_stamp.json — the process exit code lies (-run=pythonscript exits 0 even on failure). What you want: "ok": true with per-output wrote. What you might see instead:

  • status: "stale" — someone hand-edited the quest asset since the last stamp. Run quest_stamp.py extract <AssetPath> to pull the edits back into the manifest (review the diff), or re-stamp with --force to knowingly overwrite.
  • Re-stamping an unchanged manifest is a no-op (all outputs skipped) — that's normal.

The stamp writes the tag block into Config/Tags/GameplayTags.ini (via the live registry, so the tags exist in-session), creates/updates the UQuestDataAsset, and merges red-text rules.

Step 4 — Author the dialogue graphs

In the editor, create one DlgSystem dialogue asset per conversation (path must match what you put in dialogues[]). For each:

  1. Participant name = the NPC's NPCID (the C++ participant identity on AEternalNPC), not the display name.
  2. Write the nodes/branches — line text lives here, in-graph.
  3. Quest progress happens through the custom nodes: QuestDialogueEvent (StartQuest / CompleteQuest / AddQuestTag / RemoveQuestTag) on the node where the beat lands, and QuestDialogueCondition (QuestTag + require/forbid) to gate branches.
  4. Never bind quest verbs to the NPC participant — NPC-bound quest verbs are nondeterministic with 2 players and the validator errors on them. Bind to the player.
  5. Save. Saving writes the committed .dlg.json sidecar next to the asset — commit it with the asset.

Step 5 — Validate

& "C:\Program Files\Epic Games\UE_5.6\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" `
  "C:\Projects\Project-Eternal\ProjectEternal.uproject" -run=NarrativeValidation

(-ExportSidecars rewrites all sidecars first if you suspect they're stale.)

This checks the whole graph: every checked/required tag is granted somewhere (statically visible OR declared in granters[]), exact-match compendium unlocks, unique entry IDs, resolvable rewards, fresh sidecars, no NPC-bound quest verbs. Fix errors before moving on — trust the validator over any document, including this one.

Step 6 — Generate the overview

& "C:\Program Files\Epic Games\UE_5.6\Engine\Binaries\ThirdParty\Python3\Win64\python.exe" `
  Tools/Narrative/quest_overview.py

No editor needed (pure Python). Open Saved/Narrative/QuestOverview.html in a browser: per-quest map (granters → tags → dialogue flow → objectives → compendium), transcripts, and the cross-quest map. This is the "does the whole quest hang together" review artifact — read it as the designer would.

Step 7 — Play it

PIE from L_Default. Talk through the dialogue, watch the quest start, check compendium entries unlock at their beats and the category shows its name/banner. For anything granted via: map/item/blueprint that doesn't exist yet, grant the tag manually to test downstream flow (the granters[] declaration is the reminder of what still needs building).

Troubleshooting

Symptom Cause / fix
Stamp report status: "stale" Hand edit in the editor since last stamp → extract, review, re-stamp
New tag is None when used in an asset Tag wasn't registered through the stamp/editor — hand-written ini lines are invisible until restart; always let the stamp author tags
Compendium entry never unlocks requiredTag isn't granted with an exact match (child grant ≠ parent unlock)
Validator: unexplained checked tag The grant is invisible to the static scan → declare it in granters[]
Sidecar mismatch error Dialogue asset saved without its sidecar committed, or sidecar hand-edited → -ExportSidecars, commit both