Skip to content

Alert Templates (DTS)

DTS (Dynamic Template System) controls how alert messages are formatted for Discord and Telegram. Templates use Handlebars syntax.

Template Files

Templates are defined in config/dts.json. If this file doesn't exist, the bundled default from fallbacks/dts.json is used.

Additional DTS files can be placed in config/dts/ — they are merged with dts.json.

To customize templates, copy from examples/ to config/:

cp examples/dts.json config/dts.json

The examples/dts/ directory contains community-contributed templates for inspiration.

Structure

The DTS file is a JSON array of template objects. Each object specifies the alert type, platform, language, and template content:

[
  {
    "id": 1,
    "language": "en",
    "type": "monster",
    "default": true,
    "platform": "discord",
    "template": {
      "embed": { ... }
    }
  },
  {
    "id": 1,
    "language": "en",
    "type": "monster",
    "default": true,
    "platform": "telegram",
    "template": {
      "content": "...",
      "sticker": "{{{stickerUrl}}}",
      "parse_mode": "Markdown",
      "location": true,
      "webpage_preview": true
    }
  }
]

Template IDs

Each alert type can have multiple numbered templates (e.g. "id": 1, "id": 2). Users select which template to use when setting up tracking:

!track pikachu template2

The default template is controlled by [general] default_template_name.

Template Matching

Templates are matched by type, platform, and language. If no exact language match is found, the default template is used.

TOML Format

config/dts/*.toml files are loaded alongside JSON. The wire shape is the same — one entry per slot — but TOML's multi-line strings make the template body much easier to author than escaped JSON:

[[entry]]
id          = "1"
type        = "monster"
platform    = "discord"
language    = "en"
description = "Compact monster card"

template = """
{
  "embed": {
    "title": "{{fullName}} {{round iv}}%",
    "color": "{{ivColor}}"
  }
}
"""
  • Each [[entry]] is one DTS entry — same (type, id, platform, language) key as the JSON form.
  • The template field is the same Handlebars body you'd put in templateFile — TOML's """...""" strings keep newlines and braces readable without \n and \" escaping.
  • Inline operator-authored buttons attach with the [[entry.buttons]] array-of-tables syntax (see Interactive Buttons).

Loading Order

When multiple entries match the same (type, platform, id, language) key, the last-loaded entry wins. Files are loaded in this order:

  1. fallbacks/dts.json — bundled defaults (read-only, shipped with the code).
  2. config/dts.json — your main config.
  3. config/dts/*.json and config/dts/*.toml — additional files (alphabetical within the directory).

This means a monster/1/discord/en entry in config/dts/monster-1-discord-en.toml overrides the same key in config/dts.json, which overrides fallbacks/dts.json. To customise a shipped template, copy it into config/dts/ and edit there — your override always wins.

Config Editor Round-trip

The config editor speaks JSON internally and re-emits each file in its original format on save. Comments and operator-chosen key ordering are not preserved across editor saves; the entire file is rewritten. Multi-line template strings ("""...""") are preserved.

Each save takes a pre-write backup into config/backups/ so an accidental clobber is recoverable. If you hand-author elaborate TOML and want comments preserved, edit the file directly and call POST /api/dts/reload instead of going through the editor.

Per-template Save Behaviour

The POST /api/dts/templates endpoint writes each saved template to its own file in config/dts/ (e.g. monster-1-discord-en.toml). If the template previously lived in config/dts.json or another file, it is removed from the old file (other entries in that file are preserved). Source files that become empty are deleted (except config/dts.json itself).

Templates can be reloaded from disk without restarting the processor via GET /api/dts/reload. See API.md in the PoracleNG repo for the full DTS editor API.

The default: true Gotcha for Help Entries

default: true on an entry means "match any query of this (type, platform, language) whose id doesn't have an exact match" — and it does not check the id. For most types that's harmless. For type: "help" it has a subtle consequence:

  • !help queries id "index".
  • !help track queries id "track".
  • !help raid queries id "raid". ... etc.

If your custom help entry has "default": true with any id (e.g. "1"), it matches every one of those queries and silently shadows the shipped per-topic help entries.

Intent Config
My entry is the complete help (replaces all topics too) {"id": "<anything>", "default": true}
My entry is the landing page; shipped !help track / !help raid / ... still work {"id": "index", "default": false}

PoracleNG emits a startup advisory when it sees a user type: "help" entry with default: true.

Alert Types

Type Trigger
monster Wild pokemon with IV data
monsterNoIv Wild pokemon without IV data
raid Active raid boss
egg Raid egg (boss unknown)
quest Field research quest
invasion Team Rocket invasion
lure Lure module on pokestop
nest Nest pokemon change
weatherchange Weather change
gym Gym team change
fort-update Fort (gym/pokestop) metadata change
greeting Welcome message for new users

Common Variables

These variables are available across most alert types:

Location & Maps

Variable Description
{{{addr}}} Geocoded address (use triple braces for unescaped HTML)
{{city}} City name
{{zipcode}} Zip/postal code
{{country}} Country
{{state}} State/province
{{neighbourhood}} Neighbourhood
{{streetName}} Street name
{{streetNumber}} Street number
{{latitude}} Latitude
{{longitude}} Longitude
{{areas}} Comma-separated matching geofence names
{{{staticMap}}} Static map image URL
{{{googleMapUrl}}} Google Maps link
{{{appleMapUrl}}} Apple Maps link
{{{wazeMapUrl}}} Waze navigation link
{{{rdmUrl}}} RDM map link (if configured)
{{{reactMapUrl}}} ReactMap link (if configured)
{{{rocketMadUrl}}} RocketMAD link (if configured)

Images

Variable Description
{{{imgUrl}}} Primary image URL (pokemon/item/gym)
{{{imgUrlAlt}}} Alternative image URL
{{{stickerUrl}}} Telegram sticker URL

Time

Variable Description
{{time}} Formatted despawn/end time
{{tthh}} Hours remaining
{{tthm}} Minutes remaining
{{tths}} Seconds remaining
{{now}} Current time
{{nowISO}} Current time in ISO format

Discord Countdown Timers

Discord supports dynamic countdown timestamps using the <t:UNIX:R> format. Each alert type exposes the right Unix timestamp field:

Alert Type Field Snippet
Pokemon {{despawnTimestamp}} <t:{{despawnTimestamp}}:R>
Raid {{endTimestamp}} <t:{{endTimestamp}}:R>
Egg {{hatchTimestamp}} <t:{{hatchTimestamp}}:R>
Invasion {{expirationTimestamp}} <t:{{expirationTimestamp}}:R>
Lure {{expirationTimestamp}} <t:{{expirationTimestamp}}:R>
MAX Battle {{endTimestamp}} <t:{{endTimestamp}}:R>

These render as live countdowns in Discord (e.g. "in 12 minutes") that update automatically on the client. Paste the snippet directly into your DTS embed description or title.

Other

Variable Description
{{prefix}} Bot command prefix (e.g. !)
{{distance}} Distance from user's location
{{bearing}} Bearing from user's location
{{bearingEmoji}} Cardinal direction emoji

Monster Variables

Available in monster and monsterNoIv templates:

Pokemon Info

Variable Description
{{name}} Pokemon name (translated)
{{nameEng}} Pokemon name (English)
{{formName}} Form name (translated)
{{formNameEng}} Form name (English)
{{fullName}} Name + form (translated), e.g. "Pikachu Libre"
{{fullNameEng}} Name + form (English)
{{id}} Pokedex number
{{formId}} Form ID
{{generation}} Generation number
{{generationName}} Generation name (translated)
{{generationRoman}} Generation in Roman numerals
{{color}} Pokemon type color (hex)

Stats (IV Pokemon Only)

Variable Description
{{iv}} IV percentage (use {{round iv}} to round)
{{atk}} Attack IV (0-15)
{{def}} Defense IV (0-15)
{{sta}} Stamina IV (0-15)
{{cp}} Combat Power
{{level}} Pokemon level
{{weight}} Weight
{{height}} Height
{{ivColor}} Hex color based on IV tier

Moves

Variable Description
{{quickMoveName}} Fast move name (translated)
{{chargeMoveName}} Charge move name (translated)
{{quickMoveEmoji}} Fast move type emoji
{{chargeMoveEmoji}} Charge move type emoji
{{quickMoveId}} Fast move ID
{{chargeMoveId}} Charge move ID

Type & Appearance

Variable Description
{{emojiString}} Type emoji(s) combined
{{typeEmoji}} Same as emojiString
{{typeName}} Type name(s) comma-separated
{{genderData.emoji}} Gender emoji
{{genderData.name}} Gender name
{{size}} Size category
{{sizeName}} Size name (translated)
{{rarityName}} Rarity name (translated)
{{boosted}} Boolean — is weather boosted
{{boostWeatherEmoji}} Weather boost emoji (empty if not boosted)
{{boostWeatherName}} Boost weather name
{{gameWeatherName}} Current cell weather name
{{gameWeatherEmoji}} Current cell weather emoji
{{shinyPossible}} Boolean — can be shiny
{{shinyPossibleEmoji}} Shiny sparkle emoji (if possible)
{{shinyStats}} Shiny rate (if known)

PVP Rankings

Variable Description
{{pvpAvailable}} Boolean — any PVP data available
{{bestGreatLeagueRank}} Best Great League rank
{{bestUltraLeagueRank}} Best Ultra League rank
{{bestLittleLeagueRank}} Best Little League rank
{{bestGreatLeagueRankCP}} CP at best Great League rank
{{bestUltraLeagueRankCP}} CP at best Ultra League rank
{{pvpGreat}} Array of Great League rankings (see below)
{{pvpUltra}} Array of Ultra League rankings
{{pvpLittle}} Array of Little League rankings
{{pvpGreatBest}} Best Great League entry
{{pvpUltraBest}} Best Ultra League entry
{{pvpLittleBest}} Best Little League entry
{{userHasPvpTracks}} Boolean — user has PVP tracking

Each PVP ranking entry (used with {{#each pvpGreat}}) has:

  • {{rank}} — PVP rank
  • {{fullName}} — Pokemon name (may be an evolution)
  • {{cp}} — CP at league cap
  • {{level}} — Level needed
  • {{levelWithCap}} — Level with cap notation
  • {{percentage}} — Stat product percentage

Weather Forecast

Variable Description
{{weatherChange}} Formatted weather change notice
{{weatherCurrentName}} Current weather name
{{weatherCurrentEmoji}} Current weather emoji
{{weatherNextName}} Forecasted weather name
{{weatherNextEmoji}} Forecasted weather emoji
{{weatherChangeTime}} Time of weather change

Events

Variable Description
{{futureEvent}} Boolean — upcoming event affects this pokemon
{{futureEventName}} Event name
{{futureEventTime}} Event start/end time
{{futureEventTrigger}} 'start' or 'end'

Other Monster Fields

Variable Description
{{disguisePokemonName}} Ditto disguise pokemon name
{{seenType}} How seen: encounter, wild, pokestop, cell, lure
{{confirmedTime}} Whether despawn time is verified
{{catchBase}} Base catch rate %
{{catchGreat}} Great ball catch rate %
{{catchUltra}} Ultra ball catch rate %
{{baseStats}} Object with baseAttack, baseDefense, baseStamina

Raid & Egg Variables

Variable Description
{{fullName}} Raid boss name with form (empty for eggs)
{{name}} Raid boss name (translated)
{{levelName}} Raid level text (e.g. "Level 5")
{{level}} Numeric raid level
{{cp}} Boss CP
{{{gymName}}} Gym name (use triple braces)
{{{gymUrl}}} Gym image URL
{{gymColor}} Gym team color (hex)
{{teamName}} Gym team name
{{ex}} Boolean — EX-eligible gym
{{quickMoveName}} Boss fast move (translated)
{{chargeMoveName}} Boss charge move (translated)
{{quickMoveEmoji}} Fast move type emoji
{{chargeMoveEmoji}} Charge move type emoji
{{boosted}} Boolean — weather boosted
{{evolution}} Evolution type (mega)

Quest Variables

Variable Description
{{{pokestopName}}} Pokestop name
{{{questString}}} Quest task description
{{{rewardString}}} Reward description
{{with_ar}} Boolean — AR scan required
{{{pokestopUrl}}} Pokestop image URL

Invasion Variables

Variable Description
{{{pokestopName}}} Pokestop name
{{gruntType}} Grunt type name (e.g. "Dragon")
{{gruntTypeColor}} Type color (hex)
{{gruntTypeEmoji}} Type emoji
{{genderData.name}} Grunt gender name
{{genderData.emoji}} Grunt gender emoji
{{gruntRewardsList}} Reward tiers (see example below)

The gruntRewardsList has .first and .second tiers, each with chance (percentage) and monsters (array with .name).

Lure Variables

Variable Description
{{{pokestopName}}} Pokestop name
{{lureTypeName}} Lure type (e.g. "Glacial", "Mossy")
{{lureTypeId}} Lure type ID
{{lureTypeColor}} Lure type color (hex)
{{lureTypeEmoji}} Lure type emoji

Nest Variables

Variable Description
{{name}} Pokemon name (translated)
{{nestName}} Nest location name
{{pokemonSpawnAvg}} Average spawns per hour
{{resetDate}} Nest cycle start date
{{color}} Pokemon type color
{{emojiString}} Type emoji(s)

Weather Change Variables

Variable Description
{{weatherName}} New weather name
{{{weatherEmoji}}} New weather emoji
{{oldWeatherName}} Previous weather name
{{{oldWeatherEmoji}}} Previous weather emoji
{{activePokemons}} Array of affected pokemon

Each entry in activePokemons has .name, .formName, .iv, .cp.

Gym Variables

Variable Description
{{{gymName}}} Gym name
{{teamName}} New controlling team
{{previousControlName}} Previous team name
{{slotsAvailable}} Available slots (0-6)
{{trainerCount}} Number of defenders
{{color}} Team color (hex)
{{{gymUrl}}} Gym image URL
{{inBattle}} Boolean — gym under attack

Fort Update Variables

Variable Description
{{changeTypeText}} Change type: "New", "Edit", "Removal"
{{fortTypeText}} Fort type: "Gym" or "Pokestop"
{{name}} Fort name
{{description}} Fort description
{{oldName}} Previous name (on edit)
{{newName}} New name (on edit)
{{oldDescription}} Previous description
{{newDescription}} New description
{{isEditName}} Boolean — name was changed
{{isEditDescription}} Boolean — description was changed

Discord Template Format

Discord templates use an embed object:

{
  "embed": {
    "color": "{{ivColor}}",
    "title": "{{round iv}}% {{fullName}} cp:{{cp}} L:{{level}} {{atk}}/{{def}}/{{sta}} {{{boostWeatherEmoji}}}",
    "description": "End: {{time}}, Time left: {{tthm}}m {{tths}}s\n{{{addr}}}\nquick: {{quickMoveName}}, charge: {{chargeMoveName}}\nMaps: [Google]({{{googleMapUrl}}}) | [Apple]({{{appleMapUrl}}})",
    "thumbnail": {
      "url": "{{{imgUrl}}}"
    },
    "image": {
      "url": "{{{staticMap}}}"
    }
  }
}

Triple Braces

Use {{{variable}}} (triple braces) for URLs and content that may contain special characters. Double braces {{variable}} HTML-escape the output.

Telegram Template Format

Telegram templates use a content string with optional sticker, parse_mode, location, and webpage_preview fields:

{
  "content": "{{round iv}}% {{fullName}} cp:{{cp}} L:{{level}}\n{{{addr}}}\nMaps: [Google]({{{googleMapUrl}}}) | [Apple]({{{appleMapUrl}}})",
  "sticker": "{{{stickerUrl}}}",
  "parse_mode": "Markdown",
  "location": true,
  "webpage_preview": true
}

Real-World Examples

Monster Alert with PVP Rankings (from default template)

{
  "embed": {
    "color": "{{ivColor}}",
    "title": "{{round iv}}% {{fullName}} cp:{{cp}} L:{{level}} {{atk}}/{{def}}/{{sta}} {{{boostWeatherEmoji}}}",
    "description": "End: {{time}}, Time left: {{tthm}}m {{tths}}s \n {{#if weatherChange}}{{{weatherChange}}}\n{{/if}}{{#if futureEvent}}There is an event ({{futureEventName}}) {{#eq futureEventTrigger 'start'}}starting{{else}}ending{{/eq}} at {{futureEventTime}}.\n{{/if}}{{{addr}}} \n quick: {{quickMoveName}}, charge: {{chargeMoveName}} \n{{#if pvpGreat}}**Great league:**\n{{#each pvpGreat}} - {{fullName}} #{{rank}} @{{cp}}CP (Lvl. {{levelWithCap}})\n{{/each}}{{/if}}{{#if pvpUltra}}**Ultra league:**\n{{#each pvpUltra}} - {{fullName}} #{{rank}} @{{cp}}CP (Lvl. {{levelWithCap}})\n{{/each}}{{/if}} Maps: [Google]({{{googleMapUrl}}}) | [Apple]({{{appleMapUrl}}})",
    "thumbnail": {
      "url": "{{{imgUrl}}}"
    },
    "image": {
      "url": "{{{staticMap}}}"
    }
  }
}

Invasion with Conditional Reward Display

{
  "embed": {
    "title": "Team Rocket at {{{pokestopName}}}",
    "color": "{{gruntTypeColor}}",
    "description": "Type: {{gruntType}} {{gruntTypeEmoji}}\nGender: {{genderData.name}}{{genderData.emoji}}\nPossible rewards: {{#compare gruntRewardsList.first.chance '==' 100}}{{#forEach gruntRewardsList.first.monsters}}{{this.name}}{{#unless isLast}}, {{/unless}}{{/forEach}}{{/compare}}{{#compare gruntRewardsList.first.chance '<' 100}}\n ‣ {{gruntRewardsList.first.chance}}% : {{#forEach gruntRewardsList.first.monsters}}{{this.name}}{{#unless isLast}}, {{/unless}}{{/forEach}}\n ‣ {{gruntRewardsList.second.chance}}% : {{#forEach gruntRewardsList.second.monsters}}{{this.name}}{{#unless isLast}}, {{/unless}}{{/forEach}}{{/compare}}\n Ends: {{time}}, in ({{#if tthh}}{{tthh}}h {{/if}}{{tthm}}m {{tths}}s)"
  }
}

No-IV Pokemon (MonsterNoIv)

{
  "embed": {
    "color": "{{color}}",
    "title": "?% {{fullName}} {{{boostWeatherEmoji}}}",
    "description": "Ends: {{time}}, Time left: {{tthm}}m {{tths}}s \n {{{addr}}} \n Maps: [Google]({{{googleMapUrl}}}) | [Apple]({{{appleMapUrl}}})"
  }
}

Weather Change with Affected Pokemon

{
  "embed": {
    "title": "⚠️ Weather change ⚠️",
    "description": "{{#if oldWeatherName}}It went from {{oldWeatherName}} {{{oldWeatherEmoji}}} to {{else}}It is now {{/if}}{{weatherName}} {{{weatherEmoji}}}\n{{#if activePokemons}}The following Pokémon have changed:\n{{#each activePokemons}}**{{this.name}}** {{#isnt this.formName 'Normal'}} {{this.formName}}{{/isnt}} - {{round this.iv}}% - {{this.cp}}CP\n{{/each}}{{else}}This could have altered reported stats and IV...{{/if}}"
  }
}

Partials

Partials are reusable template fragments defined in config/partials.json. Include them with {{> partialName}}.

Example partial definition:

{
  "remainingTime": "{{#if tthh}}{{tthh}}h {{/if}}{{tthm}}m {{tths}}s"
}

Use in a template:

Time left: {{> remainingTime}}

Template Testing

Test your templates with the !poracle-test command:

!poracle-test pokemon
!poracle-test raid
!poracle-test pokestop
!poracle-test gym
!poracle-test nest
!poracle-test quest
!poracle-test fort-update
!poracle-test max-battle

This sends a test alert using sample data from config/testdata.json (or the bundled fallback).

For advanced template features (helpers, calculations, custom maps), see Advanced DTS.