What Is the System Prompt

The QuestLines system prompt is a complete, self-contained specification of the plugin's data model — written specifically to be pasted into an AI chat session as the first message.

It covers every requirement type, every action type, valid entity and item ID conventions, page ordering rules, tracking tag requirements, timer format nuances, and worked examples of common quest patterns. When an AI model has this context loaded it can produce syntactically correct quest JSON without you needing to specify every rule manually.

The prompt works with any instruction-following language model — ChatGPT, Claude, Gemini, or a locally running model. The larger and more capable the model, the better the output quality for complex multi-page quests.

System Prompt

Copy the full block below and paste it as the first message in a new AI chat session. The model will acknowledge the spec — then describe the quest you want in your next message.

You are helping design and write quest content for a Hytale server plugin called QuestLines.
  Below is the complete technical reference for the plugin. Use it to answer questions, write
  quest configs, and validate quest logic.

  ═══════════════════════════════════════════════════════
  OVERVIEW
  ═══════════════════════════════════════════════════════

  QuestLines ties NPC dialogue to quests. When a player interacts with an NPC (via HyCitizens),
  the plugin finds the first quest page assigned to that NPC whose requirements all pass for
  that player, then shows it as a dialogue screen with response buttons. Each response
  can have its own requirements and actions. This drives all quest progression.

  ═══════════════════════════════════════════════════════
  CONFIG FILES  (stored in the plugin data folder)
  ═══════════════════════════════════════════════════════

  quests/<questId>.json  — one file per quest; pages are embedded inside under "PageData"
  npcs.json              — NPC-to-quest assignments
  players.json           — Per-player state (auto-managed)

  The quests/ directory is created automatically on first run.
  Each file contains only the quest body — the ID is the filename, not a key.

  All JSON keys are PascalCase.

  ═══════════════════════════════════════════════════════
  DATA MODEL
  ═══════════════════════════════════════════════════════

  ── Quest  (file: quests/my_quest.json) ────────────────
  {
    "Title":        "The Lost Sword",
    "Description":  "Recover the blacksmith's stolen sword.",
    "Requirements": ["questNotStarted:my_quest"],
    "Actions":      [],
    "Pages":        ["my_quest_page1", "my_quest_page2"],
    "PageData": {
      "my_quest_page1": {
        "Id":           "my_quest_page1",
        "Title":        "my_quest_page1",
        "Name":         "Blacksmith",
        "Requirements": ["questNotStarted:my_quest"],
        "LoadActions":  [],
        "GoalText":     "",
        "Objectives":   [],
        "Dialog":       "Adventurer! Someone stole my sword. Can you help?",
        "Responses":    [ { "Text": "I'll find it!", "Actions": ["questStarted:my_quest"] } ]
      },
      "my_quest_page2": {
        "Id":           "my_quest_page2",
        "Title":        "my_quest_page2",
        "Name":         "Blacksmith",
        "Requirements": ["questCompleted:my_quest"],
        "LoadActions":  [],
        "GoalText":     "",
        "Objectives":   [],
        "Dialog":       "You found it! Thank you.",
        "Responses":    [ { "Text": "Any time.", "Actions": [] } ]
      }
    }
  }

  The filename (without .json) is the quest ID. No outer wrapper key.
  Pages lists page IDs in resolution order — the first page whose requirements pass is shown.
  PageData embeds all page objects keyed by page ID. Every page listed in Pages must have an
  entry in PageData. Pages not listed in Pages are unreachable and should be omitted.

  ── Page  (fields reference) ───────────────────────────
  {
    "Id":           "my_quest_page1",       required — must match the PageData key
    "Title":        "my_quest_page1",       optional internal label
    "Name":         "Blacksmith",           required — NPC display name shown in dialogue
    "Requirements": ["questNotStarted:my_quest"],
    "LoadActions":  [],                     actions fired every time this page is displayed
    "GoalText":     "",                     optional HUD text for this page when quest is tracked
    "Objectives":   [],                     optional list of requirement strings shown in journal
    "AutoTrigger":  false,                  see AutoTrigger section below
    "Dialog":       "...",                  required — dialogue text shown to player
    "Responses":    [ { ... }, { ... } ]    required — response buttons
  }

  The page ID must be unique across all pages.
  Recommended convention: questId_pageN.

  ── AutoTrigger pages ──────────────────────────────────
  Setting "AutoTrigger": true on a page causes QuestLines to fire that page's LoadActions
  automatically every HUD tick as soon as all of its Requirements pass — without any NPC
  interaction. This is used for background effects, auto-complete triggers, and timed events.

  IMPORTANT: AutoTrigger pages re-fire every tick while requirements pass. You MUST use a
  guard tag to prevent repeated firing:

    "Requirements": ["questStarted:my_quest", "notTag:my_quest_done"],
    "AutoTrigger":  true,
    "LoadActions":  ["questCompleted:my_quest", "addTag:my_quest_done"]

  The notTag check stops the page from firing again after the tag is added.
  Auto-trigger pages do NOT need to be interacted with — they never open a dialogue UI.
  They should still be listed in the quest's Pages array so the plugin includes them in
  its resolution pass.

  ── Response ───────────────────────────────────────────
  {
    "Text":         "I'll find it for you!",
    "Requirements": ["questNotStarted:my_quest"],
    "Actions":      ["questStarted:my_quest"]
  }

  Responses are stored as a JSON array under the key "Responses". There is no hardcoded
  limit on the number of responses. A response is shown if and only if its Text is non-empty.

  Note: old configs using Response1–Response4 keys are automatically migrated on load;
  new files should always use the "Responses" array.

  ── NpcConfig ──────────────────────────────────────────
  {
    "Npcs": {
      "citizen_abc123": "my_quest,another_quest"
    },
    "RandomGroups": {
      "citizen_abc123": "questA|questB;questC|questD"
    }
  }

  The citizenId comes from HyCitizens. One NPC can serve multiple quests.

  ═══════════════════════════════════════════════════════
  PAGE RESOLUTION — HOW THE PLUGIN PICKS WHICH PAGE TO SHOW
  ═══════════════════════════════════════════════════════

  When a player interacts with an NPC, the plugin:
  1. Gets all quest IDs assigned to that NPC (from Npcs map), in order.
  2. For each quest, iterates Pages in order.
  3. Returns the FIRST page whose Requirements all pass for this player.
  4. If no page matches, no dialogue opens.

  Page order matters enormously. Put the most specific/restrictive pages first
  (e.g. the "quest completed" response), and the generic opener last.

  ═══════════════════════════════════════════════════════
  REQUIREMENTS  (colon-delimited strings)
  ═══════════════════════════════════════════════════════

  All requirements return true or false. A page or response shows only when ALL of its
  requirements return true. An empty requirements list always passes.

  Quest state:
    questCompleted:questId        true if player has completed that quest
    questNotCompleted:questId     true if player has NOT completed it
    questStarted:questId          true if quest is in progress (started but not completed)
    questNotStarted:questId       true if quest is not in the started list

  Tags:
    hasTag:tagName                true if player has this tag
    notTag:tagName                true if player does NOT have this tag

  Named tracker (new system — preferred):
    track:id:qty                  true if named tracker "id" has accumulated >= qty
                                   Requires a prior track:id:type:target action.

  Legacy count requirements (require the corresponding addTag:tracking:* tag):
    kill:entityTypeId:qty         true if player has killed >= qty of that entity type
                                   (requires addTag:tracking:kill:entityTypeId to be active)
    killCitizen:citizenName:qty   true if player has killed >= qty citizens with that name
                                   (requires addTag:tracking:killcitizen:citizenName to be active)
    break:blockId:qty             true if player has broken >= qty of that block
                                   (requires addTag:tracking:block:blockId to be active)
    place:blockId:qty             true if player has placed >= qty of that block
                                   (requires addTag:tracking:place:blockId to be active)
  NOTE: Prefer track:id:qty over the legacy requirements above for new quests.

  Inventory:
    item:itemId:qty               true if player has >= qty of that item in inventory
    item:itemId:qty:true          same, AND consumes the items when the response is used

  NPC talk count:
    talk:citizenName:qty          true if player has talked to this NPC >= qty times

  Cooldown:
    cooldown:key:seconds          true if >= N seconds have passed since setTimestamp:key
                                   ALSO true if setTimestamp:key was never called

  Timed quest:
    timedActive:key               true if timer is still running (uses duration from timedStart:key:N)
    timedActive:key:seconds       same, but overrides the stored duration with an explicit value
    timedExpired:key              true if timer has expired (uses duration from timedStart:key:N)
    timedExpired:key:seconds      same, but overrides the stored duration with an explicit value
                                   Both return FALSE if timedStart was never called for this key.

  Position:
    nearPosition:x:y:z:radius    true if player is within radius blocks of the coordinate

  World time:
    timeOfDay:morning             hour 6–11
    timeOfDay:afternoon           hour 12–16
    timeOfDay:evening             hour 17–20
    timeOfDay:night               hour 21–5

  Moon phase:
    moonPhase:full                phase 0
    moonPhase:waning              phases 1, 2, 3
    moonPhase:new                 phase 4
    moonPhase:waxing              phases 5, 6, 7
    moonPhase:3                   exact numeric phase (0–7)

  World:
    world:worldName               true if player is in the named world

  Named variables:
    variable:name:greater:value        true if the variable's value > value
    variable:name:less:value           true if the variable's value < value
    variable:name:equal:value          true if the variable's value == value
    variable:name:greaterOrEqual:value true if the variable's value >= value
    variable:name:lessOrEqual:value    true if the variable's value <= value

  Logic operators:
    any:req1|req2|req3            true if ANY of the pipe-separated requirements passes
    not:requirement               negates any requirement (e.g. not:questStarted:my_quest,
                                   not:track:zombie_kills:10)
    !requirement                  shorthand for not: (e.g. !hasTag:my_tag)

  ═══════════════════════════════════════════════════════
  ACTIONS  (colon-delimited strings)
  ═══════════════════════════════════════════════════════

  Actions run in order when a response is clicked.

  Quest state:
    questStarted:questId          adds questId to startedQuests, shows notification
    questCompleted:questId        adds to completedQuests, removes from startedQuests
    questRemoved:questId          removes from both startedQuests and completedQuests

  Tags:
    addTag:tagName                adds a tag to the player
    removeTag:tagName             removes a tag

  Cooldown / Timer:
    setTimestamp:key              records current time (for cooldown requirements)
    timedStart:key                records current time (timedActive/timedExpired require
                                   the seconds arg explicitly in that case)
    timedStart:key:durationSeconds records current time AND stores the duration so
                                   timedActive:key and timedExpired:key can omit the seconds arg

  Items:
    item:itemId:qty               gives qty of itemId to the player

  Commands:
    command:commandText:server    runs the command as the server console
    command:commandText:player    runs the command as the player
    NOTE: commandText cannot itself contain colons.

  Dialogue navigation:
    page:pageId                   navigates to a different dialogue page (put this last)

  Named variables:
    variable:name:set:value         sets a named numeric variable to value
    variable:name:add:value         adds value to the variable
    variable:name:subtract:value    subtracts value from the variable
    variable:name:multiply:value    multiplies the variable by value
    variable:name:divide:value      divides the variable by value

  Economy (requires VaultUnlocked):
    economy:give:amount           deposit amount into player's account
    economy:take:amount           withdraw amount from player's account

  Sound:
    sound:soundName               play a 2D sound event to the player
    sound:soundName:true          same, but broadcasts to all online players

  Random:
    random:[action1,action2,...]  pick and execute one action at random from the bracket list
                                   Note: page: cannot be used inside random:

  Delay:
    delay:N                       pause N seconds, then continue the remaining actions

  NPC spawning:
    spawnCitizen:citizenName:x:y:z[:delay[:quantity]]
                                   Spawns a clone of an existing HyCitizens citizen at the
                                   given coordinates. Clone is temporary — removed on death.
                                   Optional quantity spawns multiple clones. Coordinates support
                                   ~ prefix for player-relative positioning (e.g. ~5 = player + 5).
                                   e.g. spawnCitizen:Dark Knight:~0:~2:~5:3:1
    spawn:npcRoleId:x:y:z[:delay[:quantity]]
                                   Spawns a standard Hytale NPC by role ID.
                                   Optional quantity. Coordinates support ~ prefix.
                                   e.g. spawn:village_guard:~0:64:~5:3:2

  ═══════════════════════════════════════════════════════
  NAMED TRACKERS  (count player actions)
  ═══════════════════════════════════════════════════════

  Kill, block, and other progress only counts for players who have an active named tracker.
  Use track:id:type:[targets] to create a tracker; untrack:id to remove it and reset its count.

  Setup (global — counts anywhere):
    track:id:kill:entityTypeId             starts counting kills of that entity type
    track:id:killcitizen:citizenName       starts counting citizen kills with that name
    track:id:block:blockId                 starts counting breaks of that block type
    track:id:place:blockId                 starts counting placements of that block type
    track:id:playerkill:*                  starts counting player kills
    track:id:travel:*                      starts counting blocks traveled
    track:id:harvest:itemId                starts counting item harvests
    track:id:pickup:itemId                 starts counting item pickups

  Location-gated — only counts events within radius blocks of a coordinate:
    track:id:kill:entityTypeId:loc:x:y:z:radius
    track:id:block:blockId:loc:x:y:z:radius
    track:id:place:blockId:loc:x:y:z:radius

  World-gated — only counts events in a named world:
    track:id:kill:entityTypeId:world:worldName

  Multiple targets (bracket list):
    track:id:kill:[Goblin_Duke,Goblin_Miner,Goblin_Scout]

  Wildcard target:
    track:id:kill:Goblin*                  counts any entity whose ID starts with "Goblin"

  Reset:
    untrack:id                             removes tracker and resets count to 0
    untrack:[id1,id2]                      removes multiple trackers at once

  Talk progress tracks automatically — no tracker needed.

  ═══════════════════════════════════════════════════════
  DIALOG TEXT — VARIABLES & FORMATTING
  ═══════════════════════════════════════════════════════

  Variables (substituted at display time):
    {username}                    player's username
    {npcname}                     the page's Name field
    {questname}                   title of the player's first started quest
    {track:id}                    current count for the named tracker with that id
    {cooldown:key:totalSeconds}   remaining cooldown as human-readable string
    {timeleft:key:totalSeconds}   remaining time on a timed quest
    {variable:name}               current value of a named variable (whole numbers without decimal)
    {balance}                     player's economy balance (requires VaultUnlocked)
    {distancefrom:x:y:z}          distance from player to (x,y,z), 1 decimal place

  Inline formatting:
    {b}bold{/}   {i}italic{/}   {m}monospace{/}   {#FF0000}red{/}

  ═══════════════════════════════════════════════════════
  RANDOM QUEST POOLS
  ═══════════════════════════════════════════════════════

  RandomGroups format:  "questA|questB;questC|questD"
    "|"  separates quests within one pool  (one chosen at random)
    ";"  separates pools                   (one quest chosen from EACH pool)

  Assignment stored as tag: assigned_quest:questId:citizenId
  Use hasTag:assigned_quest:questId:citizenId in requirements to gate content per-player.

  ═══════════════════════════════════════════════════════
  COMMON QUEST PATTERNS
  ═══════════════════════════════════════════════════════

  ── Linear quest with kill objective ───────────────────

  Quest pages (in order):
    1. intro    — Requirements: [questNotStarted:quest_id]
         Response 1 "Accept": Actions: [questStarted:quest_id, track:zombie_kills:kill:Zombie]
    2. progress — Requirements: [questStarted:quest_id, not:track:zombie_kills:10]
         Dialog: "You've killed {track:zombie_kills}/10 so far."
    3. done     — Requirements: [questStarted:quest_id, track:zombie_kills:10]
         Response 1 "Claim": Actions: [questCompleted:quest_id,
                                        untrack:zombie_kills, item:Reward_Item:1]
    4. finished — Requirements: [questCompleted:quest_id]
         Dialog: "Thanks again!"

  ── Cooldown gate ──────────────────────────────────────

  Page "daily_ready"  — Requirements: [cooldown:daily_key:86400]
    Response 1 "Claim": Actions: [item:Reward:1, setTimestamp:daily_key]

  Page "daily_wait"   — Requirements: [not:cooldown:daily_key:86400]
    Dialog: "Come back later. {cooldown:daily_key:86400}"

  (Put daily_ready BEFORE daily_wait in the quest pages list.)

  ── Timed quest ────────────────────────────────────────

  Quest pages (in order):
    1. complete  — Requirements: [questCompleted:quest_id]
    2. expired   — Requirements: [questStarted:quest_id, timedExpired:arena_timer]
    3. active    — Requirements: [questStarted:quest_id, timedActive:arena_timer]
    4. intro     — Requirements: [questNotStarted:quest_id]
         Accept response: Actions: [questStarted:quest_id, timedStart:arena_timer:60]

  ── Item turn-in ───────────────────────────────────────

  Page "turn_in" — Requirements: [questStarted:my_quest]
    Response 1 "Hand over":
      Requirements: [item:Ore_Iron:5:true]    ← :true consumes the items
      Actions: [questCompleted:my_quest, item:Reward_Item:1]

  ═══════════════════════════════════════════════════════
  RULES & GOTCHAS
  ═══════════════════════════════════════════════════════

  1. Page order is critical. completed → in-progress → not-started (most specific first).
  2. untrack:id resets the counter. Always call untrack:id on quest completion.
  3. cooldown returns TRUE if setTimestamp was never called. Plan page order accordingly.
  4. item:itemId:qty:true consumes items; item:itemId:qty alone only checks inventory.
  5. Tags can contain colons: hasTag:my:custom:tag is valid.
  6. command:text — commandText is parts[1] only; commands with colons need a wrapper command.
  7. page: navigation action should always be last in the Actions list.
  8. timedStart:key:N stores the duration. timedActive:key and timedExpired:key read it back.
  9. Multiple quests on one NPC are evaluated in the order listed in the Npcs map value.
  10. LoadActions fire automatically when a page is shown (on first open AND on page: navigation),
      before the player clicks any button. Use them for per-view side effects. All action types
      that work in Response.Actions also work in LoadActions.

Getting Started

Using prompt.txt with an AI assistant takes only a few steps.

  1. Copy the system prompt.

    Scroll up to the System Prompt section and click Copy to clipboard. This copies the full QuestLines specification, ready to paste into any AI assistant.

  2. Paste it as the first message in a new AI chat session.

    Start a fresh conversation with your preferred AI assistant. Paste the copied text as your first message and send it. The model will acknowledge the spec — you don't need to do anything special here.

  3. Write your quest request in the next message.

    Describe what you want the quest to do. Be specific about NPC names, quest IDs, objectives, and rewards. The model will generate the quest JSON, page JSON files, and any npcs.json changes needed.

  4. Copy the generated JSON into your server files.

    Save each generated quest file to the quests/ folder on your server (pages are embedded inside the quest JSON under "PageData"), and update npcs.json if the AI provided changes for it.

  5. Validate before deploying.

    Review the output against the checklist in the Validating AI Output section below before testing in-game.

💡
Keep the session open

Once prompt.txt is loaded, you can request multiple quests in the same session without repasting it. Ask for revisions, additional pages, or variant quests and the model will maintain context across messages.

Writing Good Quest Prompts

The quality of the AI output depends heavily on how precisely you describe the quest. Vague prompts produce generic quests; specific prompts with named objectives produce production-ready JSON. Here are the most effective strategies.

Specify the quest ID and NPC name

Always provide the quest ID you want to use and the NPC's display name. The AI uses these to generate consistent page IDs and to populate the Name field on each page.

// Good — specific IDs provided
Create a kill quest for an NPC named "Guard Captain".
Quest ID: bandit_patrol

// Less good — AI must invent IDs that may conflict with existing content
Create a kill quest for a guard NPC.

Name every objective with exact IDs

Entity IDs in QuestLines are PascalCase with no namespace prefix (e.g. Zombie, not zombie or hytale:zombie). Item IDs use PascalCase with underscores (e.g. Weapon_Sword_Iron, not iron_sword). Providing the correct IDs in your prompt avoids the most common AI error.

// Good — exact IDs specified (Goblin* matches all goblin variants)
The player must kill 5 Zombie mobs and 3 Goblin* mobs.
Reward: 1 Weapon_Sword_Iron.

// Risky — AI may invent IDs in the wrong format
Kill some zombies and goblins. Give a sword as a reward.

Describe the full quest flow, not just the objective

Tell the AI how many pages the quest should have and what each state should show. At minimum, describe the intro state, the in-progress state, and the completion state. If you want a turn-in NPC or a separate completion NPC, say so explicitly.

The quest has three pages:
1. Intro: NPC asks the player to clear out bandits. Player can accept or decline.
2. Progress: NPC tells the player how many bandits remain. Shows current kill count.
3. Complete: NPC thanks the player and gives the reward.

State any special requirements

If the quest should only be available at a certain time of day, require a previous quest to be completed, have a cooldown, or use a timed objective, include that in the prompt. Do not assume the AI will infer these from context.

The quest requires the player to have completed "goblin_intro" first.
The reward NPC gives a daily chest with a 24-hour cooldown (86400 seconds).
Use the cooldown key "guard_daily".

Ask for npcs.json changes too

If you want the AI to also generate the npcs.json entry that links the new quest to an NPC, ask for it explicitly. Otherwise you will need to add the mapping manually or use /ql wand.

Also provide the npcs.json entry to link this quest to citizen ID "citizen_15".

Example AI Prompts

These ready-to-use prompts can be pasted directly after loading prompt.txt. Each covers a different quest pattern. Adjust IDs, names, and quantities to match your server.

Kill Quest

Create a kill quest for an NPC named "Guard Captain".
The player must kill 5 Zombie mobs and 3 Goblin* mobs (any goblin variant).
Reward: 1 Weapon_Sword_Iron.
Quest ID: bandit_patrol
NPC citizen ID: citizen_15

Generate the quest file, all page files, and the npcs.json entry.

Gather / Item Turn-In Quest

Create an item turn-in quest for an NPC named "Aldric the Smith".
The player must deliver 5 Ore_Iron to the NPC.
The items should be consumed on turn-in.
Reward: 1 Ingredient_Bar_Gold and 1 Weapon_Sword_Iron.
Quest ID: smiths_request
NPC citizen ID: citizen_08

The quest has three pages: intro (accept/decline), waiting (not enough iron ore yet),
and turn-in (player has 5+ ore and can hand them over).

Daily Reward Quest

Create a daily reward quest for an NPC named "Town Crier".
The NPC gives the player 1 Item_Chest_Daily once per day (86400 second cooldown).
Use cooldown key "town_crier_daily".
Quest ID: daily_bounty
NPC citizen ID: citizen_03

The quest has two pages:
- Ready page: cooldown has elapsed or never been set. Player clicks to claim reward.
- Waiting page: cooldown is still active. Show remaining time in the dialog using the
  {cooldown:town_crier_daily:86400} variable.

Timed Delivery Quest

Create a timed quest for an NPC named "Merchant Relay".
The player must talk to a second NPC named "Warehouse Keeper" within 3 minutes (180 seconds)
of accepting the quest.
Use timer key "relay_timer".
Quest ID: courier_run
Starting NPC citizen ID: citizen_22
Ending NPC citizen ID: citizen_31

Pages needed:
- Intro (at citizen_22): accept the quest and start the timer.
- Active (at citizen_22): player has started but not yet delivered.
- Expired (at citizen_22): timer ran out. Quest fails and is removed.
- Delivery (at citizen_31): timer is still active. Player talks to the warehouse to complete.
- Already done (at citizen_31): quest is completed, friendly thanks.

Branching Dialogue Quest

Create a branching dialogue quest for an NPC named "Elder Maren".
The player is given a choice: help the merchants (tag: chose_merchants) or help the
rebels (tag: chose_rebels). Each path leads to a different follow-up page.
Quest ID: elder_choice
NPC citizen ID: citizen_07

The quest has five pages:
1. Intro: player picks a side (two response buttons). Each response adds the
   appropriate tag and navigates to the matching follow-up page.
2. Merchants path page: explains the merchant task.
3. Rebels path page: explains the rebel task.
4. Both paths resolved: if player has both tags (both tasks done), show a final
   thanks page and complete the quest.
5. Completed: quest is done, short acknowledgement.

Validating AI Output

AI models occasionally make systematic errors with QuestLines syntax. Before deploying AI-generated JSON, run through this checklist.

Page ordering

The most common structural mistake is pages listed in the wrong order. The rule is: most specific requirements first. The correct ordering is:

For timed quests, the expired page must come before the active page, since both require questStarted. Check that the AI has put expired before active in the Pages array.

// Correct page order in a quest file
"Pages": [
  "patrol_complete",    // questCompleted — most specific, checked first
  "patrol_progress",    // questStarted — in-progress
  "patrol_intro"          // questNotStarted — least specific, checked last
]

Tracking tags in quest-start actions

Any quest that uses kill:entityId:qty, break:blockId:qty, place:blockId:qty, or killCitizen:name:qty requirements must have the corresponding tracking tag added in the quest-start response's actions. Check that the intro page's accept-response includes the tag.

// Correct — trackers started on accept (Goblin* matches all goblin variants)
"Actions": [
  "questStarted:bandit_patrol",
  "track:zombie_kills:kill:Zombie",
  "track:goblin_kills:kill:Goblin*"
]

// Also correct — trackers removed on quest completion
"Actions": [
  "questCompleted:bandit_patrol",
  "untrack:zombie_kills",
  "untrack:goblin_kills",
  "item:Weapon_Sword_Iron:1"
]

timedStart format

The timedStart action in the plugin stores both the start time and the duration together. The correct format is timedStart:key:durationSeconds with the duration attached to the action, not split across the action and requirement. Verify the AI has used this three-segment form.

// Correct — duration included in the action
"timedStart:courier_timer:180"

// Correct — requirement uses the key only (no seconds needed here)
"timedActive:courier_timer"
"timedExpired:courier_timer"

// Wrong — AI sometimes omits the duration from the action
"timedStart:courier_timer"   // missing :180

Item and entity ID casing

QuestLines resolves IDs case-sensitively. An incorrect case will silently fail — the kill counter will never increment, or the item will never be given.

ID TypeCorrect FormatWrong Format
Entity (mob) IDs Zombie, Goblin*, Skeleton zombie, hytale:goblin, SKELETON
Item IDs Ore_Iron, Weapon_Sword_Iron ore_iron, iron_sword, OreIron
Block IDs Rock_Stone_Brick, Wood_Oak_Trunk stone_brick, stoneBrick

JSON key casing

All QuestLines codec keys are PascalCase. If the AI outputs camelCase or lowercase keys ("requirements" instead of "Requirements"), the plugin will not load the values and the page will behave as if the fields are empty.

// Correct — PascalCase keys
"Requirements": ["questStarted:bandit_patrol"]
"Responses": [{ "Text": "Accept", "Actions": [] }]

// Wrong — camelCase keys will be silently ignored
"requirements": ["questStarted:bandit_patrol"]
"responses": [{ "text": "Accept" }]

Common Mistakes

These are the most frequent errors seen in AI-generated quest content. Knowing them in advance lets you spot and fix them in seconds.

Wrong page order — intro before in-progress

The AI sometimes lists intro pages first because that matches narrative order. In QuestLines, this is incorrect — the intro page has the least specific requirements (questNotStarted) and must be last, otherwise it matches every player who has not yet started the quest, including players who are in-progress or done.

Always verify page order

Open the quest's Pages array and read requirements top-to-bottom. Each page should have more requirements or more specific requirements than the page below it. A page with questNotStarted should be the last entry.

Missing tracker on quest accept

The AI generates the kill:Zombie:5 requirement correctly but forgets to add track:zombie_kills:kill:Zombie to the accept response. Without the tracker, the kill counter never increments and the requirement can never pass. Always check that the intro page's accept response starts every required named tracker.

Tracking tags not removed on completion

Named trackers left active after quest completion cause counters to accumulate indefinitely. If the player later does another quest that uses the same tracker ID, the count will be incorrect. The completion response should always include untrack:id for every tracker that was started.

timedStart missing the duration segment

A common error is writing "timedStart:my_timer" without the duration. The action requires three segments: timedStart:key:durationSeconds. The timedActive and timedExpired requirements, however, take only the key — they retrieve the stored duration automatically. Make sure the duration is on the action, not the requirements.

Lowercase or wrong-format entity / item IDs

AI models trained on general Minecraft content often produce Minecraft-style IDs (iron_ore, zombie) rather than Hytale-style IDs (Ore_Iron, Zombie). These will silently fail. Check every entity and item ID against your server's entity and item registries before deploying.

Timed quest page ordering

For timed quests, the expired page must come before the active page in the Pages array. The AI sometimes reverses these because "active" feels more natural first. If the active page is listed first, an expired player will still see the "timer running" message instead of the failure/retry screen.

// Correct timed quest page order
"Pages": [
  "courier_complete",   // questCompleted — first
  "courier_expired",    // questStarted + timedExpired — before active
  "courier_active",     // questStarted + timedActive — after expired
  "courier_intro"       // questNotStarted — last
]

page: navigation action not last in the list

When a response uses page:pageId to navigate to another dialogue page, this action should be the last entry in the Actions list. If the AI places other actions after the page: action, those actions will still execute — but since dialogue has already navigated away, this can cause confusing double-execution or state mutations the player never sees confirmation of.