Skip to content

AI context pack

Writing an EMS plugin with an AI assistant? Paste this whole page into the chat before you ask for code. It's the entire public API on one page. An assistant that hasn't seen it guesses at method names and gets them wrong; one that has it writes against the real surface.

This page is condensed from the rest of these docs and tracks them. The fuller versions are linked from the API reference. To paste a clean copy, grab the raw markdown.

How to use it

  1. Paste everything below the line into your assistant.
  2. Describe the plugin you want, concretely.
  3. Check the result against this page: every GRIPEMS.API:... call it writes must appear below. If a method isn't here, it doesn't exist, and the assistant invented it. Tell it so and have it redo that part.

The AI-assisted plugin guide has example prompts and the full check-it loop.


GRIP-EMS plugin API, condensed

EMS (GRIP - Enhanced Macro Sequencer) is a World of Warcraft Retail addon. A plugin is its own addon that extends EMS through one frozen table, GRIPEMS.API (global alias _G.GRIPEMS_API). You never edit EMS and never reach into _G.GRIPEMS. Everything a plugin contributes through its handle is reverted the moment the user disables it, so the addon returns to stock.

API_VERSION: 3. Documented against GRIP-EMS 2.3.7 on WoW Retail 12.0.7.

Hard rules (follow every one)

  1. The plugin is a separate addon. Its .toc has ## Dependencies: GRIP-EMS, so WoW loads EMS first and refuses to load the plugin without it.
  2. Do setup from PLAYER_LOGIN; do UI setup after the GEMS_UI_READY event. Never at file scope.
  3. Handshake before any other call: null-check GRIPEMS.API, then API:RequireVersion(n).
  4. To contribute anything that should vanish on disable, call API:RegisterPlugin(id, meta) and work through the handle it returns.
  5. Use only methods on this page. Never read or write _G.GRIPEMS.
  6. A variable provider's Resolve and a condition's Evaluate return a plain, non-secret scalar (string, number, or boolean). Never a table, a function, or a 12.0 secret value such as UnitHealth("player").
  7. Registering or changing returns ok first: true, or false plus a reason. Read accessors return the value or nil and never raise. Call with the colon: API:Method().

Plugin skeleton (the correct shape)

-- MyPlugin.toc
-- ## Interface: 120007
-- ## Title: My EMS Plugin
-- ## Author: You
-- ## Version: 1.0.0
-- ## Dependencies: GRIP-EMS
-- MyPlugin.lua

local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_LOGIN")
f:SetScript("OnEvent", function()
    local API = GRIPEMS and GRIPEMS.API
    if not API then return end                    -- EMS not loaded
    if not API:RequireVersion(2) then return end  -- needs v2 for the handle

    local handle = API:RegisterPlugin("myplugin", {
        name = "My EMS Plugin",
        version = "1.0.0",
        OnEnable = function(h)
            -- register providers, author sequences, mount panels here
        end,
        OnDisable = function(h)
            -- only cleanup EMS can't do; it reverts your contributions for you
        end,
    })
    if not handle then return end
end)

Tier 0 - discovery

  • GRIPEMS.API.API_VERSION -> integer (3)
  • GRIPEMS.API.EMS_VERSION -> string ("2.3.7"); for logs only, don't gate features on it
  • GRIPEMS.API:RequireVersion(n) -> true, or false + reason
  • GRIPEMS.API:GetCapabilities() -> array of capability id strings
  • GRIPEMS.API:RegisterPlugin(id, meta) -> handle, or nil + reason
    • meta = { name = string, version = string, OnEnable = function(handle), OnDisable = function(handle) }
    • id is your namespace for everything you contribute; a duplicate id is rejected, never overwritten.

Capability ids: events, data, sequences, ui, preview, variables, conditions, stepfunctions, plugins, authoring, panels, views, settings, cvars, stepdata, macro, slash. Check GetCapabilities() before using a tier if you want to degrade on an older EMS.

Tier 1 - events (listen only)

  • GRIPEMS.API:On(event, handler) -> handle string, or false + reason. The handler gets the payload args only, not the event name. It runs inside pcall.
  • GRIPEMS.API:Off(handle) -> nothing
  • GRIPEMS.API:ListEvents() -> array of the subscribable names

On rejects any name not in this set. Payloads that carry a table hand you a deep-copied, read-only snapshot.

Event Payload
SEQUENCE_CREATED (name, data)
SEQUENCE_DELETED (name)
SEQUENCE_IMPORTED (results)
SEQUENCE_UPDATED (name, data)
SEQUENCE_STEP_ADVANCED (seqName, step, numSteps)
KEYBIND_CHANGED (seqName, key) - both may be nil on a bulk/clear change
CONTEXT_CHANGED (newContext, oldContext)
LOADOUT_CHANGED (newID, newName, oldID, oldName)
SETTING_CHANGED (key, value)
PLUGIN_REGISTERED (name, version)
PLUGIN_SEQUENCES_LOADED none
GEMS_UI_READY none - do UI setup on this one
GEMS_UI_LAYOUT_APPLY none
GEMS_UI_NAV_CHANGED (viewId)
GEMS_EDITOR_TAB_CHANGED (tabName)
GEMS_SEQUENCE_SELECTED (name)
GEMS_PREVIEW_MODE_CHANGED (mode)
GEMS_PREVIEW_UPDATE none

PLUGIN_ENABLED and PLUGIN_DISABLED are not public; use your OnEnable / OnDisable callbacks instead.

Tier 2 - data (read-only; you get copies and scalars)

  • GRIPEMS.API:GetSequenceList() -> array of { name, stepCount, currentStep, stepFunction }
  • GRIPEMS.API:GetSequenceInfo(name) -> table or nil. Fields: name, stepFunction, versionCount, defaultVersion, activeVersionIndex, activeStepCount, contextVersionCount, classID, specID, author, description, help, helplink, changelog, talentString, url, privacyMode, version, createdAt, updatedAt, disabled, keybind, variableDeps
  • GRIPEMS.API:GetSequenceSteps(name) -> array of { index, spellID, spellName, icon } for the active version, or nil. Public scalars (never secret); the per-step view for action-bar chrome. Pair with SEQUENCE_STEP_ADVANCED. Capability stepdata.
  • GRIPEMS.API:GetAuthoredSteps(name) -> array of { index, spellID, spellName, icon } in AUTHORED base order (the order the user wrote), or nil. Interleave copies suppressed and the version repeatCount not applied; Loops unrolled and IF branches flattened, so it is the base order, not the tree. Distinct from GetSequenceSteps' EXECUTION domain -- its indices do NOT map to currentStep or the SEQUENCE_STEP_ADVANCED step index. Capability stepdata. Added in EMS 2.3.7 with NO API_VERSION change, and stepdata predates it, so neither RequireVersion nor the capability list proves it is present -- guard every call with if API.GetAuthoredSteps then ... end, which is safe because a missing key reads as nil rather than raising.
  • GRIPEMS.API:GetSequenceMacroIndex(name) -> macro slot index or nil (read-only; never creates the macro). Capability macro.
  • GRIPEMS.API:GetCurrentContext() -> string ("none", "Raid", "Arena", ...)
  • GRIPEMS.API:GetSetting(key) -> value or nil. Allowlist: uiLayout (string|nil), debug (boolean). For any other setting, listen to SETTING_CHANGED.
  • GRIPEMS.API:GetRegisteredPlugins() -> array of { name, version, loaded, sequenceCount }

Tier 3 - UI and layout (GRIPEMS.API.UI)

  • :RegisterLayoutProvider(id, provider) -> ok
  • :SetActiveLayoutProvider(id) -> ok. Persists uiLayout; call only when the user opts in, not on load.
  • :GetActiveLayoutProvider() -> id string
  • :GetHost(name) -> frame or nil. Hosts: navHost, listHost, metadataHost, editorHost, configHost, globalViewHost, iconFooterHost
  • :MountPanel(panelId, host) -> ok. Content panels: sequenceList, editor, variables, conditions, macros, source. preview goes through the Preview facade instead. options, import, export, about are dialogs and ignore the host. vehiclePet, metadata are reserved and return false.
  • :SetClassicChrome(enabled) -> ok. false hides the default two-panel chrome so yours shows.
  • :RegisterView(id, def) -> ok. def = { id, name }
  • :SetActiveView(id) -> ok. Fires GEMS_UI_NAV_CHANGED.
  • :GetActiveView() -> id or nil

Layout provider table. EMS calls the callbacks as plain functions (no self):

local provider = {
    id = "acme_modern",          -- must equal the id you register under
    name = "Acme Modern",
    OnRegister      = function(self) end,
    OnInitMainFrame = function(mainFrame, hosts) end,  -- before panels
    OnInitPanels    = function(mainFrame, hosts) end,  -- build chrome into hosts here
    OnApplyLayout   = function(context) end,           -- reposition on a layout pass
    OnMinimize      = function(mainFrame) end,
    OnRestore       = function(mainFrame) end,
}

SAFETY: hosts are plain, non-secure, reparentable frames. Never place a secure frame or SecureActionButtonTemplate under a host; reparenting it taints the secure execution path. Keep any secure frames your plugin makes outside the hosts.

Tier 3 - preview (GRIPEMS.API.Preview)

  • :GetMode() -> "icons" | "text" | "compiled"
  • :SetMode(mode) -> ok (one of the three modes)
  • :Update(version) -> true (the version arg is optional/advisory)
  • :Hide() -> nothing
  • :MountSidebar(host) -> ok
  • :MountIconFooter(host) -> ok

Tier 4 - registries

  • GRIPEMS.API:RegisterVariableProvider(id, spec) -> ok
    • spec = { id, name, Resolve = function(self, varName) ... end, OnRegister? }
    • Resolve returns a plain non-secret scalar for names it owns, nil otherwise. Runs at compile time; keep it pure and cheap.
  • GRIPEMS.API:RegisterCondition(id, spec) -> ok
    • spec = { id, name, Evaluate = function(self) ... end, OnRegister? } ; Evaluate returns a boolean.
  • GRIPEMS.API:EvaluateCondition(id) -> clean boolean (users call this inside a variable body)
  • GRIPEMS.API:RegisterStepFunction(id, spec) -> ok
    • spec = { id, name, Expand = function(self, resolvedStepTexts) ... end, OnRegister? } ; Expand returns an array of macrotext strings in execution order. Pure. Each entry of resolvedStepTexts is itself a full macrotext line after variable substitution (e.g. "/cast [combat] Kill Command"), NOT a bare spell name -- keying a lookup on spell names silently no-ops.
  • GRIPEMS.API:RegisterSequences(name, version, seqNames, seqTable) -> ok (ships a static set; a user sequence wins on a name clash)

Reversible forms on the handle take the spec alone (id read from spec.id): handle:RegisterVariableProvider(spec), handle:RegisterCondition(spec), handle:RegisterStepFunction(spec), handle:RegisterLayoutProvider(provider).

Tier 5 - authoring (handle methods only; owner-scoped; reverted on disable)

  • handle:CreateSequence(name, data) -> ok (re-applying a name you own is a safe upsert)
  • handle:UpdateSequence(name, data) -> ok
  • handle:DeleteSequence(name) -> ok
  • handle:SelectSequence(name) -> ok (UI; fires GEMS_SEQUENCE_SELECTED)
  • handle:OpenEditor(name) -> ok (UI)
  • handle:RegisterSetting(def) -> ok. def = { key, type, name, desc, ... } (e.g. min, max, step for a "range")
  • handle:OverrideSetting(key, value) -> ok
  • handle:RevertSetting(key) -> ok
  • handle:RequestCVarProfile(profileKey) -> ok (routed through the EMS CVar manager; no direct secure-CVar write)
  • handle:RevertCVarProfile() -> ok
  • handle:RegisterImportProvider(spec) -> ok. spec = { id, name, Parse = function(self, text) ... end, Detect? }
  • handle:RegisterExportProvider(spec) -> ok. spec = { id, name, Serialize = function(self, seq) ... end }
  • handle:EnsureSequenceMacro(name) -> macro slot index, or false + reason. Creates the sequence's action-bar macro if absent (journaled, deleted on disable); an existing EMS/user macro is returned without journaling. Guards in-combat, slot-pool-full, and unknown sequence. PickupMacro(index) to drag it onto a bar. Capability macro.
  • handle:RegisterSlashCommand(sub, handler, helpText?) -> ok, or false + reason. Registers a /gems subcommand; handler(argString) runs in pcall; a built-in or duplicate name is rejected; journaled, removed on disable. Capability slash.
  • handle:GetId() -> your plugin id

Ownership is strict: a plugin may create, edit, or delete only sequences it owns. A user's sequence and another plugin's objects are rejected.

Tier 5 - theme (GRIPEMS.API.UI)

  • :RegisterPanelFrame(frame, category, class) -> makes your frame inherit the active EMS skin. Example: (myFrame, "panel", "panel.acmeNav"). Appearance only, no structural authority. Namespace class to your plugin.

The lock list (no public write path, ever)

Rotation execution; secure buttons and the keybind matrix; authorship and signing; peer-to-peer transmission; direct secure-CVar writes; raw persistence; taint-laundering and secret values. The rule: if a call would change which spell fires, how a key binds, who authored a sequence, what is transmitted, or what is saved, it does not exist on the public API. You can read derived state (active version index, context, summaries); you cannot write any of these. To put a sequence on an action bar, use handle:EnsureSequenceMacro -- the generated 255-char macro is the supported bar path; setting a keybind from a plugin stays locked.

Secret values

WoW 12.0 tags some values (a unit's health, for example) as secret so they can't cross into protected code. A variable provider that returns one has it rejected and the variable resolves to nothing; a condition that returns one evaluates to false. Return computed or non-secret values (counts, settings, group size, derived numbers).