A player buys a sandwich from your shop, opens their bag, sees it sitting there with a tidy label, clicks it, and nothing happens. Nothing in F8, nothing in the server console, just an ESX inventory showing them a very good picture of a sandwich that will never be food.
I once watched a server owner lose an evening to that. He rewrote the SQL, restarted six times, reinstalled the inventory resource and posted in two Discords about es_extended being broken. The item was fine. The registration was in a client file, where it does nothing at all.
ESX items are deceptively small. One database row, one callback, done. That smallness is why almost every long-running server has a drawer of items that display, stack and do nothing. What follows is the path an item takes, in the order you hit it: the items table, weight against limits, the usable callback, images, metadata, and what changes the day you move to ox_inventory.
What the ESX items table actually stores
ESX keeps item definitions in one MySQL table, items, whose columns depend on your ESX version. On Legacy, adding an item is this:
INSERT INTO items (name, label, weight, rare, can_remove)
VALUES ('sandwich', 'Sandwich', 1, 0, 1);
What each column does:
nameis the primary key and the only string code ever refers to. Lowercase, no spaces. Your callback, your shop config and your image filename all key off this exact value.labelis display only. Rename it whenever, nothing downstream cares.weightis what one unit costs against the carry limit.raremarks an item that survives death, in the setups that honour the flag. Plenty of loot scripts ignore it, so verify.can_removeset to 0 means the player cannot drop it or hand it over. Correct for licences, quietly infuriating when you set it by mistake on something tradeable.
Older ESX, meaning 1.1 and the forks that never moved on, has a limit column where Legacy has weight: a per item maximum count, with -1 meaning "use the global default". If your table has limit, you are on the old model and half the guides you find do not apply.
Then the trap that eats afternoons: es_extended reads the items table once, at start. Insert a row into a running server and the item does not exist as far as the game is concerned, however many times the player reopens their bag. Restart es_extended. The confusion is fair: the SQL succeeded.
Worth knowing where the contents live, too. On ESX Legacy a player's inventory is JSON in the users table, so every pickup, drop and purchase rewrites part of that row. It is one of the hottest write paths on a busy server, and the first place to look when you start tuning the database properly.
Weight, limits, and what "full" means
Two different models get called "the inventory being full", and knowing which you run saves a lot of arguing with players.
The old per item limit model caps each type separately. Twenty bandages, ten burgers, whatever else you carry. Easy to reason about, and it gives you a character sprinting around with forty distinct objects and no sense of burden.
ESX Legacy uses a total weight budget. Each item's weight is multiplied by its count, summed, and checked against Config.MaxWeight in the es_extended config.lua. The shipped default is deliberately small, to force choices. Your instinct will be to raise it the moment players complain. Resist for a week, because a cap nobody hits is a slower version of infinite pockets.
Neither model is slot based. The base ESX inventory has no grid, no "this takes two squares", no dragging a rifle into a particular box. It is a list with a number beside each name. Slot based inventories, which is what most people picture, arrive with ox_inventory later on. While you are setting weights, pick a unit and stick to it, or you end up with a phone that weighs as much as a car door.
Registering a usable item, where most ESX items die
This is where the sandwich failed. A database row makes an item displayable, not usable. Usability is a server side registration:
-- server.lua
ESX.RegisterUsableItem('sandwich', function(source)
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.removeInventoryItem('sandwich', 1)
TriggerClientEvent('esx_status:add', source, 'hunger', 200000)
TriggerClientEvent('esx:showNotification', source, 'That was mostly bread.')
end)
How you get ESX into scope depends on your version, and it is the biggest single source of "my whole file does not run". Legacy 1.2 and up uses the shared object export:
ESX = exports["es_extended"]:getSharedObject()
Older setups triggered esx:getSharedObject in a loop until the object came back. Recent Legacy builds let you add es_extended's shared import file to your fxmanifest.lua, after which ESX is simply a global. All three appear in tutorials without saying which version they belong to.
Four ways the registration silently does nothing:
- It is in a client script.
ESX.RegisterUsableItemis server only. Nothing warns you. - The file errored on a line above it, usually while getting the shared object, so the registration never ran. That error prints at resource start, not when the player clicks, which is why nobody sees it.
- The name string does not match the
namecolumn.Sandwichis notsandwich, and neither issandwhich. - The resource is not started, or starts before es_extended in
server.cfg.
And one way it half works: forget removeInventoryItem and you have shipped an infinite item. Players will find it before you do, and will not tell you.
Where you put these registrations matters more as the list grows. Fifty usable items in one 900 line server file is a merge conflict generator, which is the same argument for splitting a resource along sensible seams that applies everywhere else on your server. One file per feature area, registration next to the logic it triggers.
Item images and the folder that fails silently
Your item now works and shows as a blank square, which every player reads as "broken".
Images are files on disk inside the inventory resource, matched by filename to the name column. For esx_inventoryhud and its forks that is an images folder under the resource's html directory, commonly html/img/items. Forks move it, so check where the existing PNGs actually sit rather than trusting any guide, this one included. For ox_inventory it is web/images.
The filename is the item name exactly, lowercase, usually .png, so sandwich.png for sandwich. A missing image is not an error: the item still works, you get an empty box, nothing is logged. And players cache NUI assets, so someone who already loaded the page can keep seeing the blank after you fix it.
The common failure is a working item with a missing icon, and an owner who reads the blank square as broken code and rewrites a callback that was correct. Check the folder before you touch the Lua.
Metadata, and why old ESX cannot have any
The base ESX inventory stores names and counts. That is the entire data model. There is no room in it for "this bottle is half empty", "this ID card belongs to Marie" or "this weapon has 340 rounds through it".
That constraint shapes ESX servers without anyone naming it. It is why you see five separate ESX items for five quality levels, and why a serial number ends up in a side table keyed by something other than the item.
Weapons are the loudest example. In base ESX they are not items at all. They live in the player's loadout, separate from the inventory with ammo tracked alongside, which is why weapon handling always feels like a different system with different rules. QBCore made weapons ordinary items carrying metadata, solving serials and inventing fresh problems around ammo. The QBCore treatment of weapons and ammo is worth reading on an ESX server, because it shows what you are buying when you move to a metadata capable inventory.
What genuinely changes with ox_inventory
Four things, and only one of them is the pretty grid.
Item definitions leave the database. They live in ox_inventory/data/items.lua as Lua tables:
['sandwich'] = {
label = 'Sandwich',
weight = 100,
stack = true,
close = true,
description = 'Slightly dry.',
client = {
status = { hunger = 200000 },
usetime = 2500,
}
},
That change alone is worth the migration. Your item list becomes a file in version control, identical on staging and live, and restorable from git when someone deletes half of it.
Weight moves to grams and slots become real. ox_inventory enforces a slot count and a weight cap together, both set through convars, so a full bag is something players can see rather than a number they collide with. That sandwich at 100 is 100g, so you will be rewriting every weight from your ESX table. Do it deliberately instead of multiplying the old numbers by a thousand.
Metadata is first class. Items carry arbitrary data per stack, so serials, owners and remaining quantity work with no side tables. Durability rides along: give an item a degrade time and it decays over real minutes, which is how you get bandages that expire.
Usable items are wired differently too. Instead of one central registration function, an item definition can point at an export in your own resource, so behaviour lives with the resource that owns it. ox_inventory also exposes server exports for adding, removing, searching and carry checks, and new code should call those.
The compatibility layer is real but partial. ox_inventory replaces a chunk of the ESX player object's inventory functions in place, so scripts calling the old add and remove functions largely keep working, and older usable item registrations are generally still honoured. What breaks is anything iterating the raw inventory table, anything writing the users inventory column directly, and any resource shipping its own inventory UI. Grep for direct inventory reads first, because those fail quietly with an empty list rather than with an error.
The migration order that does not lose player pockets
Sequence matters more than speed here.
- Stand ox_inventory up on a test server against a copy of the live database. A copy.
- Reach item parity before converting anything. Every row in
itemsneeds a definition initems.lua, because items without one do not survive. Write a query that dumps the table and generates the Lua rather than typing four hundred entries. - Move the images across and rename them at the new path.
- Run the conversion ox_inventory ships against the copy, then log in as real players and check their pockets. Pick hoarders, not fresh characters.
- Redeclare shops, stashes and drop points in ox_inventory's data files. They do not come across from your old shop resources.
- On the cutover, take the server down properly, take a fresh backup, run the conversion once, and remove the old inventory resource from
server.cfgbefore starting back up.
Two rules with teeth: never run the conversion twice, and never run it with the old inventory resource still starting. Both give you inventories that look fine to you and wrong to the player who owned them.
The short version
An item is a row plus a registration plus an image, and each part fails differently. The row without a restart means the item does not exist. The registration in a client file means it does nothing. The missing image means it looks broken while working perfectly. Check all three before rewriting code and most ESX inventory mysteries end in about ninety seconds.
Move to ox_inventory when you want metadata, durability and item definitions you can review, and migrate in the order above rather than the order that feels fastest. Then go and use your sandwich, which by then will be a real sandwich with a weight, an icon, a decay timer and a note about who made it.