Two files.
One bot.
A Lua plugin decides where to go and what to pull. A YAML strategy decides how to fight it. The duel is the line between them.
plugin.on_tick = function()
local c = clients:First()
if c:IsInCombat() then return end
local mob = c.entity:Nearest('Kraken')
if not mob then return end
c:Teleport(mob.position)
endphases:
setup:
priority: 50
when: { no_blade: storm }
actions:
- blade: { school: storm }
execute:
actions:
- cast: { type: damage }What each one does
Two scripting layers let you automate the game without writing anything but your own plugin and strategy files.
libscript embeds a sandboxed Lua VM and exposes the game client to it. You
write a .lua file, drop it in ~/.kebab/plugins, and start it — from the desktop app's
trainer, or from the console. Your plugin gets a tick callback, event hooks, hotkeys, and an
API for reading player state, finding entities, teleporting, clicking UI, and driving quests.
This is where you write the loop: the thing that decides where to go and what to do next.
libstrat is a YAML format for combat. Instead of encoding spell logic in Lua, you describe a fight as prioritised phases, each with a condition and an ordered list of actions. The combat runner reads the file and plays the round. This is where you describe how to fight, and it is deliberately not Lua — combat decisions are pure data so they are validated when the file loads rather than blowing up mid-duel.
Most real automation uses both. A farm bot is a libscript plugin that finds a mob and pulls it, then hands the fight to a libstrat strategy:
Which one do I want?
| You want to… | Use |
|---|---|
| Walk somewhere, click a thing, watch a stat, run a loop | libscript |
| Pick which spell to cast this round | libstrat |
| React to zone changes, dialogs, deaths, level-ups | libscript events |
| Blade before you hit, heal below 30%, flee on a wipe | libstrat phases |
| Coordinate two accounts through a dungeon | libscript for movement, libstrat members: for the fight |
The dividing line is the duel. Once combat starts, the strategy runner owns the client
until the fight ends. Your plugin should stop issuing movement and wait for
on_combat_exit. A plugin that teleports mid-fight moves the character out of the duel
circle while the duel is still running.
What you need
-
The Kebab Suite installed — either the desktop app, or the terminal console. A plugin file does not care which one loaded it; both run the same Lua API and the same strategy format.
-
A game client running and hooked. The desktop app hooks clients for you; in the console,
hookis the first command of every session. -
The two folders. Neither is created on a fresh install:
mkdir -p ~/.kebab/plugins ~/.kebab/strategiesPlugins are
.luafiles in the first, strategies are.ymlor.yamlfiles in the second. Loading a file is not the same as running it — you still start a plugin explicitly, from the trainer or with the console'sscripts runcommand. -
Optionally, editor support. Pointing your editor at the shipped
kebab.luadefinitions gives you autocomplete and type checking for the whole API. See Editor setup.
Automation drives the client the way a player does: it reads memory, writes position and similar data, and clicks the real UI. It never runs code inside the game process. That constraint is why some things are done the long way — dialogs are skipped by pressing space, not by calling the game's own dialog function.
A ten-second taste
A plugin that drinks a potion whenever health drops below 40%:
---@type KebabPlugin
plugin = { name = 'sipper', version = '1.0.0' }
plugin.on_tick = function()
local c = clients:First()
if not c or not c:IsConnected() or c:IsInCombat() then return end
if c:HealthBelow(40) then
local used, err = c:UsePotion()
if used then utils.Log('drank a potion') end
if err then utils.Log('potion failed: ' .. tostring(err)) end
end
end
A strategy that blades once and then throws storm at the boss:
$schema: https://scm.kebab.sh/strat/config.json
name: storm-hitter
school: storm
phases:
setup:
priority: 50
when:
no_blade: storm
actions:
- blade: { school: storm }
execute:
priority: 10
actions:
- cast: { type: damage, school: storm, target: boss }
- wand_hit: { target: first }
fallback: pass
That strategy is called storm-hitter because of its name: field, not because of what you
saved the file as. Two strategy files sharing a name: silently overwrite each other.
Where to go next
| Start here | If you want to… |
|---|---|
| libscript → Getting started | Write your first plugin and run it under both hosts |
| libstrat → Getting started | Stop your wizard passing every round |
| Cookbook | Copy a complete, working setup — farming, gardening, two-account teams, drop tracking |
| libscript → Plugin lifecycle | Know exactly which callback fires when |
| libstrat → Troubleshooting | Work out why a strategy loads but does nothing |
The Cookbook is the fastest route if you would rather adapt something that already works than build one from scratch. Every recipe there is derived from a plugin or strategy that ships with your installation.