Rotation JSON
Updated October 18, 2026
The JSON is the complete description of an advanced rotation: everything the KRC text has, plus spell IDs, slot flags and profile settings. The builder shows it on the Text and JSON page, and KeyRota accepts it as a file (New — From file…, Import). The format is called keyrota-rotation, version 1. There is a JSON Schema to check it.
Example file#
The shortest useful rotation: Rupture at 5+ combo points when it isn't on the target, otherwise Mutilate.
{
"format": "keyrota-rotation",
"version": 1,
"title": "Assassination: minimum",
"specID": 259,
"resources": ["PlayerHP", "TargetHP", "Energy", "Combo_Points"],
"auras": [{"n": 1, "alias": "Rupture_DoT", "label": "Rupture on target [1943]", "spell": 1943}],
"slots": [
{"n": 2, "alias": "Rupture", "label": "Rupture", "spell": 1943},
{"n": 4, "alias": "Mutilate", "label": "Mutilate", "spell": 1329}
],
"rules": [
{
"use": "Rupture",
"name": "Rupture",
"when": [{"all": ["Combo_Points >= 70", "Rupture_DoT.down"], "comment": "5+ combo points, no bleed"}]
},
{"use": "Mutilate", "name": "Mutilate"}
]
}
Everything the file leaves out takes its default: slots show With a target and have no flags, the enemy counter is 8 yards, enemies in combat only. A rule without when always holds.
Top-level fields#
| Field | Type | Required | Default | What it is |
|---|---|---|---|---|
format | string | yes | — | Always "keyrota-rotation". |
version | integer | yes | — | Always 1. A newer one is refused: "update KeyRota". |
title | string | no | "" | Rotation name: the first text line, the name in the log. No double quotes. |
specID | integer 0–100000 | no | 0 | WoW specialization, 0 = any. Needed for the Better Assistant profile. |
ignoreMainIcon | true / false | no | true | On: press only by the rules. Off: when no rule matches, press the game's suggestion. |
enemies | object | no | {"range": 8, "combatOnly": true} | The enemy counter for the enemies condition. |
notes | string or list of strings | no | [] | Description: # lines at the top of the text. |
resources | list of strings and null | no | [] | Names of resource1…N in order. |
auras | list | no | [] | Auras buff1…12. |
slots | list | no | [] | Slots slot1…12. |
rules | list | no | [] | Rules in priority order. |
footer | string or list of strings | no | [] | Comments after the last rule. |
Any other field is an error, "unexpected field": typos are not swallowed. So don't add "$schema" to the file; attach the schema in your editor or validator.
Almost everywhere null may stand for a value; it means the same as leaving the field out.
format and version#
"format": "keyrota-rotation" and "version": 1 are required. If version is above 1, KeyRota refuses: "version 2 is newer than this KeyRota understands: update it". Without version: "version 1 required".
title#
A string without double quotes ". Line breaks become spaces. In KRC text it is rotation "…". A good place for the version: "Assassination v1.3".
specID#
The game's specialization ID. It decides the rotation's resources (when created in the builder) and the class and specialization in the Better Assistant profile. 0 means any specialization: the rotation works, but the Better Assistant profile won't build ("Choose a specialization: the Better Assistant profile needs it").
| Class | Specialization | specID | Specialization resources (resource3…) |
|---|---|---|---|
| Warrior | Arms | 71 | Rage |
| Warrior | Fury | 72 | Rage |
| Warrior | Protection | 73 | Rage |
| Paladin | Holy | 65 | Mana, Holy_Power |
| Paladin | Protection | 66 | Mana, Holy_Power |
| Paladin | Retribution | 70 | Mana, Holy_Power |
| Hunter | Beast Mastery | 253 | Focus |
| Hunter | Marksmanship | 254 | Focus |
| Hunter | Survival | 255 | Focus |
| Rogue | Assassination | 259 | Energy, Combo_Points |
| Rogue | Outlaw | 260 | Energy, Combo_Points |
| Rogue | Subtlety | 261 | Energy, Combo_Points |
| Priest | Discipline | 256 | Mana, Insanity |
| Priest | Holy | 257 | Mana, Insanity |
| Priest | Shadow | 258 | Mana, Insanity |
| Death Knight | Blood | 250 | Runes, Runic_Power |
| Death Knight | Frost | 251 | Runes, Runic_Power |
| Death Knight | Unholy | 252 | Runes, Runic_Power |
| Shaman | Elemental | 262 | Mana, Maelstrom |
| Shaman | Enhancement | 263 | Mana, Maelstrom |
| Shaman | Restoration | 264 | Mana, Maelstrom |
| Mage | Arcane | 62 | Mana, Arcane_Charges |
| Mage | Fire | 63 | Mana, Arcane_Charges |
| Mage | Frost | 64 | Mana, Arcane_Charges |
| Warlock | Affliction | 265 | Mana, Soul_Shards |
| Warlock | Demonology | 266 | Mana, Soul_Shards |
| Warlock | Destruction | 267 | Mana, Soul_Shards |
| Monk | Brewmaster | 268 | Energy, Stagger |
| Monk | Mistweaver | 270 | Mana |
| Monk | Windwalker | 269 | Energy, Chi |
| Druid | Balance | 102 | Mana, Astral_Power |
| Druid | Feral | 103 | Energy, Combo_Points |
| Druid | Guardian | 104 | Rage |
| Druid | Restoration | 105 | Mana, Astral_Power |
| Demon Hunter | Havoc | 577 | Fury |
| Demon Hunter | Vengeance | 581 | Fury |
| Demon Hunter | Devourer | 1480 | Fury |
| Evoker | Devastation | 1467 | Mana, Essence |
| Evoker | Preservation | 1468 | Mana, Essence |
| Evoker | Augmentation | 1473 | Mana, Essence |
ignoreMainIcon#
true (default): KeyRota presses only by the rules; when none matches, it waits. false: it then presses the game's suggestion, like the Blizzard profile. The suggestion is the last resort after all the rules; it never overtakes a rule.
enemies#
"enemies": {"range": 8, "combatOnly": true}
| Field | Type | Default | What it is |
|---|---|---|---|
range | integer 1–100 | 8 | The enemy counter radius in yards. |
combatOnly | true / false | true | Count only enemies already in combat. |
The enemies condition reads this counter (see Enemies nearby). In the builder these are "Enemy counter radius, yards" and "Only enemies in combat" on the General page. The values also go into the Better Assistant profile.
notes and footer#
The rotation's description (notes) and the comments after the last rule (footer). A list of strings, or one string with \n. An empty string "" is a blank line in the text. Blank lines at the edges are dropped.
"notes": ["Based on a guide and top logs, patch 12.1", "", "v1.1: Envenom earlier on 4+ targets"]
In KRC text these are # … lines before the declarations. They don't affect how the rotation works.
resources#
Resource names in order: the first item is resource1, the second resource2, and so on. The number decides what the resource is; the name is only how you write it in conditions:
resource1is the player's health,resource2the target's health;resource3and on are the specialization's resources in the order of the specID table: for a rogueresource3is energy,resource4combo points.
"resources": ["PlayerHP", "TargetHP", "Energy", "Combo_Points"]
Use the standard names from the table: the builder shows them in plain words ("Player health", "Energy"), and other people and AI agents understand the conditions right away. null skips a number: ["PlayerHP", null, "Energy"] declares only resource1 and resource3. An undeclared resource can't be read: "resource2 is not declared".
All resources are percentages of the maximum, 0–100. How to turn combo points, energy and runes into percentages: Resources in Conditions.
auras#
Auras are your buffs and your debuffs on the target. Rotation aura n is aura n in the Better Assistant list.
| Field | Type | Required | Default | What it is |
|---|---|---|---|---|
n | integer 1–12 | yes | — | Aura number (buffN). Each number once. |
alias | string | no | from label | The name in conditions. See Names. |
label | string | no | "" | Display name. No double quotes ". Handy with the ID in brackets: "Garrote on target [703]". |
spell | integer 0–100000000 | no | 0 | The aura's Wowhead ID. 0 = not set: the aura won't be in the Better Assistant profile and will be unknown. |
note | string | no | "" | A comment at the end of the declaration line. |
"auras": [
{"n": 1, "alias": "Garrote_DoT", "label": "Garrote on target [703]", "spell": 703},
{"n": 2, "alias": "Rupture_DoT", "label": "Rupture on target [1943]", "spell": 1943}
]
The order in the list doesn't matter, n does. Numbers may have gaps, but for the Better Assistant profile the auras with IDs must go in a row from the first.
slots#
Slots are the rotation's buttons. Rotation slot n is slot n in Better Assistant.
| Field | Type | Required | Default | What it is |
|---|---|---|---|---|
n | integer 1–12 | yes | — | Slot number (slotN). |
alias | string | no | from label | The name in rules and conditions. |
label | string | no | "" | The spell's name. No double quotes ". |
spell | integer 0–100000000 | no | 0 | The Wowhead spell ID. Better Assistant uses it to put the spell into the slot. |
note | string | no | "" | A comment at the end of the declaration line. |
show | "target" / "combat" | no | "target" | When Better Assistant shows the slot. |
manual | true / false | no | false | Flag M. |
channel | true / false | no | false | Flag C. |
offGcd | true / false | no | false | Flag O. |
queue | true / false | no | false | Flag Q, only with offGcd. |
show#
"target": the slot is shown when you have a target. True for almost every attack."combat": the slot is shown in combat even without a target. For healing and defensives: Crimson Vial must work when there's no target.
A slot is ready only when Better Assistant shows it. A "target" slot without a target isn't ready, and its rules are skipped.
Slot flags#
Flags say how to press a slot once its rule is chosen. In the builder they are the How to press switches.
| Flags | In JSON | How it's pressed | Example |
|---|---|---|---|
| none | all false | In the queue window at the end of the GCD: the spell goes off as soon as the GCD ends. | Mutilate, Envenom |
| M | "manual": true | Never. The slot's rules are skipped. Pressing its key yourself pauses the rotation. | An ability you press yourself |
| C | "channel": true | Like a normal spell; while the channel goes, nothing is pressed. After the press the slot isn't pressed again until the channel starts. | Killing Spree (51690) |
| O | "offGcd": true | At once, even mid-GCD. | Keep It Rolling (381989) |
| O + Q | "offGcd": true, "queue": true | Off the GCD, but only in the queue window at the end of the GCD. | The Adrenaline Rush + Killing Spree macro (13750); Vanish (1856) after Kingsbane |
| C + O | "channel": true, "offGcd": true | A channel that starts off the GCD: at once, then nothing until the channel ends. | Rare |
| C + O + Q | all three | The same, in the queue window. | Rare |
| Q without O | "queue": true | Error: "queue only together with offGcd". | — |
| M + others | "manual": true, … | M wins: the slot isn't pressed, the other flags don't matter. | — |
Why O + Q and not just O: a button with O alone is pressed at once, mid-GCD, before the previous hit has landed. Vanish after Kingsbane with O alone would be pressed before Kingsbane's combo points arrive, and the Vanish rule (Combo_Points < 65) would check the old value. With Q the button waits for the end of the GCD.
Strict priority: an off-GCD rule lower in the list doesn't overtake a higher rule that is waiting for the GCD.
"slots": [
{"n": 1, "alias": "Adrenaline_Rush", "label": "Adrenaline Rush (macro with Killing Spree)", "spell": 13750, "offGcd": true, "queue": true},
{"n": 4, "alias": "Keep_It_Rolling", "label": "Keep It Rolling", "spell": 381989, "offGcd": true},
{"n": 8, "alias": "Killing_Spree", "label": "Killing Spree", "spell": 51690, "channel": true},
{"n": 11, "alias": "Crimson_Vial", "label": "Crimson Vial", "spell": 185311, "show": "combat"}
]
rules#
Rules in priority order: the first in the list is checked first.
| Field | Type | Required | Default | What it is |
|---|---|---|---|---|
use | string | yes | — | The slot to press: its alias or slotN. |
name | string | no | "" | The action's name, shown in the log. In the text, the comment on the use line. |
note | string or list of strings | no | [] | Notes above the rule (# lines before use). |
when | list of groups | no | [] | OR groups. Empty or missing: a rule without conditions, pressed whenever the spell can be used. |
when: groups and conditions#
| Field | Type | Required | What it is |
|---|---|---|---|
all | non-empty list of strings | yes | The group's conditions, all must hold (AND). |
comment | string | no | The group's comment (in the text, at the end of its line). |
{
"use": "Garrote",
"name": "Garrote",
"note": ["Refresh in the pandemic window"],
"when": [
{"all": ["Garrote_DoT.down"], "comment": "no DoT"},
{"all": ["Garrote_DoT.up", "Garrote_DoT.duration <= 30"], "comment": "30% or less left"}
]
}
One slot may appear in several rules: for example, Garrote from Stealth high in the list and ordinary Garrote upkeep lower down.
Conditions in JSON#
Each condition is a string in KRC syntax: Garrote_DoT.down, Rupture_DoT.duration <= 25, Kingsbane.cd <= 3, Combo_Points >= 70, enemies >= 2, target.distance <= 8, cd == 1. The full list is in Conditions.
- Refer by name (
Garrote_DoT) or by number:buff1,slot3,resource4. andinside a string splits it into two conditions:"Combo_Points >= 70 and Energy >= 70"is two conditions, and both count toward the limit of 16.- No
#and no line breaks in a string. - KeyRota's own engine reads the conditions, so the JSON and the text always mean the same.
Names#
A name (alias) is how auras, slots and resources are called in conditions and in use.
- The first character is a letter (any alphabet) or
_; then letters, digits0–9and_. Cyrillic is fine; spaces, hyphens and dots are not:Fan_of_Knives, notFan-of-Knives. - A name is unique among all auras, slots and resources.
- Reserved:
use,when,and,or,rotation,gcd,enemies,target,aoe,st,cd,defensives,ground_spells,movement, and names likebuffN,slotN,resourceN. - Keywords are lowercase only:
use,or,.up.Use,ORand.UPare errors.
A name from the label. Without alias, KeyRota makes one from label: brackets and what's inside them are dropped, letters and digits stay, everything else becomes _. for example, "Garrote on target [703]" becomes Garrote_on_target. An empty or unusable name becomes Aura_N or Slot_N; a taken one gets _2, _3. If a rule refers to buffN or slotN missing from the lists, they are declared automatically as Aura_N and Slot_N.
Required fields and defaults#
| Where | Required | Defaults |
|---|---|---|
| File | format, version | title "", specID 0, ignoreMainIcon true, enemies {"range": 8, "combatOnly": true}, empty lists |
| Aura | n | alias from label, label "", spell 0 |
| Slot | n | show "target", all flags false, spell 0 |
| Rule | use | name "", note [], when [] (no conditions) |
| Group | all (non-empty) | comment "" |
Limits#
| What | Limit |
|---|---|
| Auras | 12 (n 1–12) |
| Slots | 12 (n 1–12) |
| OR groups in a rule | 16 |
| AND conditions in a group | 16 |
| Rules | unlimited |
| Number in a condition | integer 0–100 |
enemies.range | 1–100 |
spell | 0–100000000 |
specID | 0–100000 |
| Resources | up to 99 |
Exactly 16 groups of 16 conditions is fine; 17 is an error.
Errors#
A rotation with an error doesn't load at all: there is no partial execution. An error always names the place. In JSON it's a path: rules[3].when[1]: unknown name Foo (counting from zero). In the builder, "Rule 4, group 2: unknown name Foo" (counting from one). In KRC text, a line number.
JSON structure:
| Message | Cause |
|---|---|
unexpected field “offgcd” | A misspelt or unknown field (case matters: offGcd). |
missing field “format” | A required field is missing. |
format: expected “keyrota-rotation” | Another value of format. |
version: version 1 required | No version, or below 1. |
slots[0]: missing number n (1–12) | A slot or aura without n. |
slots[2].n: number outside 1..12 | n out of range. |
slots[3]: number 4 is already taken | Two slots (or two auras) with the same n. |
slots[1].show: expected “target” or “combat” | Another value of show. |
slots[0]: queue only together with offGcd | queue: true without offGcd: true. |
rules[2].use: no slot “Garotte” | use refers to a slot that doesn't exist. |
rules[0].when[1].all: a non-empty list of conditions is required | An empty group or no all. |
rules[0].when[0].all[2]: condition is empty or contains # or a line break | An empty condition string, or # in it. |
….spell: expected an integer | Wrong type: a string instead of a number, true instead of a number and so on. |
Conditions and declarations (the same as in KRC text):
| Code | Example message | Cause |
|---|---|---|
unknown_ref | unknown name Foo | No aura, slot or resource with that name. |
unknown_ref | resource5 is not declared | The resource isn't declared in resources. |
wrong_kind | Garrote is a slot, expected a buff | An aura property on a slot (Garrote.up) or the other way round. |
bad_condition | can't parse condition "Energy => 50" | A typo in the condition or the operator. |
bad_condition | "Garrote.distance <= 8": only target has distance | Only the target has a distance. |
value_range | value 120 outside 0..100 | A number above 100. There are no negative or fractional numbers. |
toggle_value | "cd >= 1": a toggle only takes == 0 or == 1 | A toggle is compared only with == and 0 or 1. |
bad_alias | invalid name Fan-of-Knives | Invalid characters in a name. |
reserved_alias | name cd is reserved | A reserved name. |
duplicate_alias | name Garrote is already declared | Two auras, slots or resources with one name. |
index_range | buff13: number outside 1..12 | A reference to a number above 12. |
too_many_groups | more than 16 OR groups | More than 16 groups in a rule. |
too_many_conditions | 17 conditions in a group (maximum 16) | More than 16 conditions in a group. |
The builder also reports "the group has no conditions" (an empty group), "no slot selected" and "quotes aren't allowed in the name".
KRC text#
KRC text is the same rotation as lines. The Text and JSON page shows it, and you can keep it as a .txt file. Text and JSON convert into each other without losing meaning.
rotation "Assassination: minimum"
buff1 Rupture_DoT = "Rupture on target [1943]"
slot2 Rupture = "Rupture"
slot4 Mutilate = "Mutilate"
resource1 PlayerHP
resource2 TargetHP
resource3 Energy
resource4 Combo_Points
use Rupture when # Rupture
Combo_Points >= 70 and Rupture_DoT.down # 5+ combo points, no bleed
use Mutilate when # Mutilate
Lines#
#outside double quotes starts a comment up to the end of the line. Inside a"label",#is an ordinary character.- Blank lines and comment-only lines are skipped: that's how descriptions and separators are written.
- Any other line is a declaration,
use,oror a condition line. Otherwise it's astray_lineerror: "line outside a rule: "…" (start prose with #)". - Any spaces and tabs between parts. The canonical form uses a tab after
buffN/slotNand at the start of condition lines.
Declarations#
Only before the first use:
rotation "Name" # optional, at most once
buff1 Garrote_DoT = "Garrote on target [703]" # aura 1
slot1 Garrote = "Garrote" # slot 1
resource1 PlayerHP # resource 1
buffN and slotN are 1–12, resourceN 1–99. The quoted label may be empty: slot5 Filler = "". A declaration after the first use is a late_declaration error.
Rules#
use Rupture when # Rupture
Combo_Points >= 70 and Rupture_DoT.down # no DoT
or
Combo_Points >= 70 and Rupture_DoT.up and Rupture_DoT.duration <= 30 # pandemic
use Slot whenopens a rule; a comment on that line is the action's name.- Each condition line is one group (conditions joined with
and). Groups are separated by a line with the single wordor. - A rule without condition lines has no conditions.
- Two condition lines in a row without
oris asplit_grouperror;orat the start, at the end or twice in a row isempty_group.
Text and JSON: how they map#
| In the text | In JSON |
|---|---|
rotation "…" | title |
# … lines before the first use | notes |
A comment at the end of a buffN / slotN line | The aura's / slot's note |
# … lines right before use (and between the rule's groups) | The rule's note |
A comment on the use line | The rule's name |
| A comment at the end of a condition line | The group's comment |
# … lines after the last rule | footer |
| — (not in the text) | specID, ignoreMainIcon, enemies, spell, show, flags |
Blank lines at the edges of comment blocks are dropped; inside they stay as "".
Import and storage#
- KeyRota keeps the rotation inside the profile. Export in the main window saves the KeyRota profile (JSON) with its rotation; Import accepts a KeyRota profile and a bare rotation JSON.
- New — From file… accepts
.txtwith KRC text, rotation JSON and a KeyRota profile; New — From clipboard, the same from the clipboard. - In the builder: Text and JSON — Paste or Open… replace the rotation; slots and auras with the same numbers keep their IDs, With a target / In combat and flags if the new version has none.
- A text rotation (without JSON) takes the slots' IDs and visibility from the Better Assistant profile already stored in the KeyRota profile, and the auras' IDs also from
[703]marks in their labels.
The Better Assistant profile from JSON#
"Build from slots and auras" in the builder makes the Better Assistant profile string from the rotation JSON:
- 12 slots: slot N is
slots[n = N].spell, shown byshow. A slot without an ID stays empty. - Auras in order from the first:
auras[n = 1].spell,auras[n = 2].spell… An aura without an ID in the middle is an error. - Class and specialization from
specID. - Enemy counter from
enemies.rangeandenemies.combatOnly. - If the KeyRota profile already holds a Better Assistant profile of the same specialization, its other settings and the matching slots and auras are kept.
In the game: /ba — Profile — paste — Apply.
JSON Schema#
The format's schema: keyrota-rotation.schema.json (draft 2020-12). It checks fields, types, ranges, queue without offGcd and the shape of condition strings. It doesn't check that n and names are unique, that names in conditions exist, or the limit of 16 after splitting strings at and : KeyRota does that on load.
To check a file:
npx ajv-cli validate --spec=draft2020 -s keyrota-rotation.schema.json -d my-rotation.json
Don't put a "$schema" field into a rotation file: KeyRota takes it for an unexpected field.