Create a WoW AddOn in 15 Minutes

This guide describes how to make a simple HelloWorld addon, use slash commands and store user settings.
==Getting started==
* You need a basic understanding of Lua, otherwise see Introduction to Lua or other tutorials.
* A simple text editor like VS Code or Notepad++.
==Hello World==
===Running scripts===
You can execute Lua scripts from the chat window or in a macro with the /run or /script command. There is no difference between them.
/run print("Hello World!")
To quickly turn scripts like this into an addon, just remove the "/run" part and paste it into https://addon.bool.no/
===Creating an AddOn===
An addon consists of Lua/XML files and a TOC file. We won't be using XML since most things that are possible in XML can also be done in Lua.
Go to your AddOns folder and create a new folder with the following files:World of Warcraft\_retail_\Interface\AddOns\HelloWorld
HelloWorld.lua
print("Hello World!")
HelloWorld.toc
 ## Interface: 
 ## Version: 1.0.0
 ## Title: Hello World
 ## Notes: My first addon
 ## Author: YourName
 
 HelloWorld.lua
* The name of the TOC file must match the folder name or the addon won't be detected by the game.
* The TOC Interface metadata as returned by () tells which version of the game the addon was made for. If they don't match then the addon will be marked out-of-date in the addon list.
Load up World of Warcraft, the addon should show up in the addon list and greet you upon login.
Tutorial_CreateAddOn_2.png|VS Code
Tutorial_CreateAddOn_3.png|AddOn List
Tutorial_CreateAddOn_4.png|In-game
==Development tips==
See also: Lua Coding Tips
* When updating addon code use /reload to test the new changes, you may want to put it on a macro hotkey; as well as temporarily disabling any unnecessary addons that would increase loading time.
* Get an error reporting addon like BugSack or turn on /console scriptErrors 1
* There is the /dump slash command for general debugging, /etrace for showing events and /fstack for debugging visible UI elements.
* Export, clone, download or bookmark Blizzard's user interface code a.k.a. the FrameXML. If you don't know what a specific API does it's best to just reference it in FrameXML. Not everything is documented so we generally look through the code from Blizzard or other addons.
* For VS Code the Lua extension by Sumneko adds IntelliSense features like code completion.
==Responding to events==
Almost every action in the game is an Event which tells the UI that something happened. For example fires when someone sends a message in a chat channel like General and Trade. 
To respond to events you create a frame with () and register the events to it.
local function OnEvent(self, event, ...)
	print(event, ...)
end
local f = CreateFrame("Frame")
f:RegisterEvent("CHAT_MSG_CHANNEL")
f:SetScript("OnEvent", OnEvent)
Another example, to play a sound on levelup with () or () you register for the event.
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_LEVEL_UP")
f:SetScript("OnEvent", function()
	PlayMusic(642322) -- sound/music/pandaria/mus_50_toast_b_hero_01.mp3
end)
===Handling multiple events===
When registering multiple events it can be messy if there are a lot of them.
local function OnEvent(self, event, ...)
	if event == "ADDON_LOADED" then
		local addOnName = ...
		print(event, addOnName)
	elseif event == "PLAYER_ENTERING_WORLD" then
		local isLogin, isReload = ...
		print(event, isLogin, isReload)
	elseif event == "CHAT_MSG_CHANNEL" then
		local text, playerName, _, channelName = ...
		print(event, text, playerName, channelName)
	end
end
local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("CHAT_MSG_CHANNEL")
f:SetScript("OnEvent", OnEvent)
Which can be refactored to this:
local f = CreateFrame("Frame")
function f:OnEvent(event, ...)
	self[event](self, event, ...)
end
function f:ADDON_LOADED(event, addOnName)
	print(event, addOnName)
end
function f:PLAYER_ENTERING_WORLD(event, isLogin, isReload)
	print(event, isLogin, isReload)
end
function f:CHAT_MSG_CHANNEL(event, text, playerName, _, channelName)
	print(event, text, playerName, channelName)
end
f:RegisterEvent("ADDON_LOADED")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("CHAT_MSG_CHANNEL")
f:SetScript("OnEvent", f.OnEvent)
==Slash commands==
:FrameXML: RegisterNewSlashCommand()
Slash commands are an easy way to let users interact with your addon. Any SLASH_* globals will automatically be registered as a slash command.
 -- increment the index for each slash command
 SLASH_HELLOW1 = "/helloworld"
 SLASH_HELLOW2 = "/hw"
 
 -- define the corresponding slash command handler
 SlashCmdList.HELLOW = function(msg, editBox)
 	local name1, name2 = strsplit(" ", msg)
 	if #name1 > 0 then -- check for empty string
 		print(format("hello %s and also %s", name1, name2 or "Carol"))
 	else
 		print("Please give at least one name")
 	end
 end
We can also add a shorter /reload command.
SLASH_NEWRELOAD1 = "/rl"
SlashCmdList.NEWRELOAD = ReloadUI
==SavedVariables==
To store data or save user settings, set the SavedVariables in the TOC which will persist between sessions. You can /reload instead of restarting the game client when updating the TOC file.
 ## Interface: 
 ## Version: 1.0.0
 ## Title: Hello World
 ## Notes: My first addon
 ## Author: YourName
 ## SavedVariables: HelloWorldDB
 
 HelloWorld.lua
SavedVariables are only accessible once the respective event fires. This example prints how many times you logged in (or reloaded) with the addon enabled.
local function OnEvent(self, event, addOnName)
	if addOnName == "HelloWorld" then -- name as used in the folder name and TOC file name
		HelloWorldDB = HelloWorldDB or {} -- initialize it to a table if this is the first time
		HelloWorldDB.sessions = (HelloWorldDB.sessions or 0) + 1
		print("You loaded this addon "..HelloWorldDB.sessions.." times")	
	end
end
local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:SetScript("OnEvent", OnEvent)
This example initializes the SavedVariables with default values. It also updates the DB when new keys are added to the defaults table. The CopyTable() function is defined in FrameXML. () is an API function.
local defaults = {
	sessions = 0,
	someOption = true,
	--someNewOption = "banana",
}
local function OnEvent(self, event, addOnName)
	if addOnName == "HelloWorld" then
		HelloWorldDB = HelloWorldDB or {}
		self.db = HelloWorldDB -- makes it more readable and generally a good practice
		for k, v in pairs(defaults) do -- copy the defaults table and possibly any new options
			if self.db[k] == nil then -- avoids resetting any false values
				self.db[k] = v
			end
		end
		self.db.sessions = self.db.sessions + 1
		print("You loaded this addon "..self.db.sessions.." times")
		print("someOption is", self.db.someOption)
		local version, build, _, tocversion = GetBuildInfo()
		print(format("The current WoW build is %s (%d) and TOC is %d", version, build, tocversion))
	end
end
local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:SetScript("OnEvent", OnEvent)
SLASH_HELLOW1 = "/hw"
SLASH_HELLOW2 = "/helloworld"
SlashCmdList.HELLOW = function(msg, editBox)
	if msg == "reset" then
		HelloWorldDB = CopyTable(defaults) -- reset to defaults
		f.db = HelloWorldDB 
		print("DB has been reset to default")
	elseif msg == "toggle" then
		f.db.someOption = not f.db.someOption
		print("Toggled someOption to", f.db.someOption)
	end
end
Tips for troubleshooting tables:
* /dump HelloWorldDB or /tinspect HelloWorldDB or /run for k, v in pairs(HelloWorldDB) do print(k, v) end shows the contents of a (global) table.
* /run wipe(HelloWorldDB) or /run for k in pairs(HelloWorldDB) do HelloWorldDB[k] = nil end empties the table.
* /run HelloWorldDB = nil; ReloadUI() removes the table reference and reloads the UI; this essentially completely resets your savedvariables.
==Options Panel==
It would be more user-friendly to provide a graphical user interface. This example prints a message when you jump, if the option is enabled. It also opens the options panel with the slash command.
FrameXML:
* InterfaceOptions_AddCategory()
* InterfaceOptionsCheckButtonTemplate
* UIPanelButtonTemplate
local f = CreateFrame("Frame")
local defaults = {
	someOption = true,
}
function f:OnEvent(event, addOnName)
	if addOnName == "HelloWorld" then
		HelloWorldDB = HelloWorldDB or CopyTable(defaults)
		self.db = HelloWorldDB
		self:InitializeOptions()
		hooksecurefunc("JumpOrAscendStart", function()
			if self.db.someOption then
				print("Your character jumped.")
			end
		end)
	end
end
f:RegisterEvent("ADDON_LOADED")
f:SetScript("OnEvent", f.OnEvent)
function f:InitializeOptions()
	self.panel = CreateFrame("Frame")
	self.panel.name = "HelloWorld"
	local cb = CreateFrame("CheckButton", nil, self.panel, "InterfaceOptionsCheckButtonTemplate")
	cb:SetPoint("TOPLEFT", 20, -20)
	cb.Text:SetText("Print when you jump")
	-- there already is an existing OnClick script that plays a sound, hook it
	cb:HookScript("OnClick", function(_, btn, down)
		self.db.someOption = cb:GetChecked()
	end)
	cb:SetChecked(self.db.someOption)
	local btn = CreateFrame("Button", nil, self.panel, "UIPanelButtonTemplate")
	btn:SetPoint("TOPLEFT", cb, 0, -40)
	btn:SetText("Click me")
	btn:SetWidth(100)
	btn:SetScript("OnClick", function()
		print("You clicked me!")
	end)
	InterfaceOptions_AddCategory(self.panel)
end
SLASH_HELLOW1 = "/hw"
SLASH_HELLOW2 = "/helloworld"
SlashCmdList.HELLOW = function(msg, editBox)
	InterfaceOptionsFrame_OpenToCategory(f.panel)
end
This reference example has a couple of checkboxes (with related functionality) and a reset button.
Multiple options with reset button  
GitHub source
HelloWorld.toc
 ## Interface: 
 ## Version: 1.0.2
 ## Title: Hello World
 ## Notes: My first addon
 ## Author: YourName
 ## SavedVariables: HelloWorldDB
 
 Core.lua
 Options.lua
Core.lua
HelloWorld = CreateFrame("Frame")
function HelloWorld:OnEvent(event, ...)
	self[event](self, event, ...)
end
HelloWorld:SetScript("OnEvent", HelloWorld.OnEvent)
HelloWorld:RegisterEvent("ADDON_LOADED")
function HelloWorld:ADDON_LOADED(event, addOnName)
	if addOnName == "HelloWorld" then
		HelloWorldDB = HelloWorldDB or {}
		self.db = HelloWorldDB
		for k, v in pairs(self.defaults) do
			if self.db[k] == nil then
				self.db[k] = v
			end
		end
		self.db.sessions = self.db.sessions + 1
		print("You loaded this addon "..self.db.sessions.." times")
		local version, build, _, tocversion = GetBuildInfo()
		print(format("The current WoW build is %s (%d) and TOC is %d", version, build, tocversion))
		self:RegisterEvent("PLAYER_ENTERING_WORLD")
		hooksecurefunc("JumpOrAscendStart", self.JumpOrAscendStart)
		self:InitializeOptions()
		self:UnregisterEvent(event)
	end
end
function HelloWorld:PLAYER_ENTERING_WORLD(event, isLogin, isReload)
	if isLogin and self.db.hello then
		DoEmote("HELLO")
	end
end
-- note we don't pass `self` here because of hooksecurefunc, hence the dot instead of colon
function HelloWorld.JumpOrAscendStart()
	if HelloWorld.db.jump then
		print("Your character jumped.")
	end
end
function HelloWorld:COMBAT_LOG_EVENT_UNFILTERED(event)
	-- it's more convenient to work with the CLEU params as a vararg
	self:CLEU(CombatLogGetCurrentEventInfo())
end
local playerGUID = UnitGUID("player")
local MSG_DAMAGE = "Your %s hit %s for %d damage."
function HelloWorld:CLEU(...)
	local timestamp, subevent, _, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags = ...
	local spellId, spellName, spellSchool
	local amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing, isOffHand
	local isDamageEvent
	if subevent == "SWING_DAMAGE" then
		amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing, isOffHand = select(12, ...)
		isDamageEvent = true
	elseif subevent == "SPELL_DAMAGE" then
		spellId, spellName, spellSchool, amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing, isOffHand = select(12, ...)
		isDamageEvent = true
	end
	if isDamageEvent and sourceGUID == playerGUID then
		-- get the link of the spell or the MELEE globalstring
		local action = spellId and GetSpellLink(spellId) or MELEE
		print(MSG_DAMAGE:format(action, destName, amount))
	end
end
SLASH_HELLOW1 = "/hw"
SLASH_HELLOW2 = "/helloworld"
SlashCmdList.HELLOW = function(msg, editBox)
	InterfaceOptionsFrame_OpenToCategory(HelloWorld.panel_main)
end
Options.lua
HelloWorld.defaults = {
	sessions = 0,
	hello = false,
	mushroom = false,
	jump = true,
	combat = true,
	--someNewOption = "banana",
}
local function CreateIcon(icon, width, height, parent)
	local f = CreateFrame("Frame", nil, parent)
	f:SetSize(width, height)
	f.tex = f:CreateTexture()
	f.tex:SetAllPoints(f)
	f.tex:SetTexture(icon)
	return f
end
function HelloWorld:CreateCheckbox(option, label, parent, updateFunc)
	local cb = CreateFrame("CheckButton", nil, parent, "InterfaceOptionsCheckButtonTemplate")
	cb.Text:SetText(label)
	local function UpdateOption(value)
		self.db[option] = value
		cb:SetChecked(value)
		if updateFunc then
			updateFunc(value)
		end
	end
	UpdateOption(self.db[option])
	-- there already is an existing OnClick script that plays a sound, hook it
	cb:HookScript("OnClick", function(_, btn, down)
		UpdateOption(cb:GetChecked())
	end)
	EventRegistry:RegisterCallback("HelloWorld.OnReset", function()
		UpdateOption(self.defaults[option])
	end, cb)
	return cb
end
function HelloWorld:InitializeOptions()
	-- main panel
	self.panel_main = CreateFrame("Frame")
	self.panel_main.name = "HelloWorld"
	local cb_hello = self:CreateCheckbox("hello", "Do the |cFFFFFF00/hello|r emote when you login", self.panel_main)
	cb_hello:SetPoint("TOPLEFT", 20, -20)
	local cb_mushroom = self:CreateCheckbox("mushroom", "Show a mushroom on your screen", self.panel_main, self.UpdateIcon)
	cb_mushroom:SetPoint("TOPLEFT", cb_hello, 0, -30)
	local cb_jump = self:CreateCheckbox("jump", "Print when you jump", self.panel_main)
	cb_jump:SetPoint("TOPLEFT", cb_mushroom, 0, -30)
	local cb_combat = self:CreateCheckbox("combat", "Print when you damage a unit", self.panel_main, function(value)
		self:UpdateEvent(value, "COMBAT_LOG_EVENT_UNFILTERED")
	end)
	cb_combat:SetPoint("TOPLEFT", cb_jump, 0, -30)
	local btn_reset = CreateFrame("Button", nil, self.panel_main, "UIPanelButtonTemplate")
	btn_reset:SetPoint("TOPLEFT", cb_combat, 0, -40)
	btn_reset:SetText(RESET)
	btn_reset:SetWidth(100)
	btn_reset:SetScript("OnClick", function()
		HelloWorldDB = CopyTable(HelloWorld.defaults)
		self.db = HelloWorldDB
		EventRegistry:TriggerEvent("HelloWorld.OnReset")
	end)
	InterfaceOptions_AddCategory(HelloWorld.panel_main)
	-- sub panel
	local panel_shroom = CreateFrame("Frame")
	panel_shroom.name = "Shrooms"
	panel_shroom.parent = self.panel_main.name
	for i = 1, 10 do
		local icon = CreateIcon("interface/icons/inv_mushroom_11", 32, 32, panel_shroom)
		icon:SetPoint("TOPLEFT", 20, -32*i)
	end
	InterfaceOptions_AddCategory(panel_shroom)
end
function HelloWorld.UpdateIcon(value)
	if not HelloWorld.mushroom then
		HelloWorld.mushroom = CreateIcon("interface/icons/inv_mushroom_11", 64, 64, UIParent)
		HelloWorld.mushroom:SetPoint("CENTER")
	end
	HelloWorld.mushroom:SetShown(value)
end
-- a bit more efficient to register/unregister the event when it fires a lot
function HelloWorld:UpdateEvent(value, event)
	if value then
		self:RegisterEvent(event)
	else
		self:UnregisterEvent(event)
	end
end
==AddOn namespace==
The addon namespace is a private table shared between Lua files in the same addon. This way you can avoid leaking variables to the global environment.
HelloWorld.toc
 ## Interface: 
 ## Version: 1.0.0
 ## Title: Hello World
 
 FileA.lua
 FileB.lua
FileA.lua
local _, ns = ...
ns.foo = "Banana"
FileB.lua
local addonName, ns = ...
print(addonName, ns.foo) -- prints "HelloWorld" and "Banana"
Or you can simply use a unique global variable.
FileA.lua
MyAddon = {}
MyAddon.value = 0
function MyAddon:DoSomething(increment)
	self.value = self.value + increment
end
MyAddon:DoSomething(2)
FileB.lua
MyAddon:DoSomething(3)
print(MyAddon.value) -- 5
==Conclusion==
You know how to write a simple addon from scratch! Go and publish it on CurseForge (guide), WoWInterface (guide) and/or wago.io.If you want to cheat and rather start with a complete example it's available here: HelloWorld.zip
: Follow-up: Ace3 for Dummies
See also
* MMO-Champion: Creating Your Own WoW Addon