Guide · Updated 14 Sep 2026
Using Poggy Core in your own RedM scripts: one framework API for VORP, RSG and QBCore
Poggy Core (Free) is the free framework layer every Poggy script runs on, and it does not care whether the script calling it is ours. If you write RedM resources, for one server or for sale, you can build on it today: one set of verbs for characters, money, jobs, items, storage, notifications, menus and text inputs, and poggy_core translates them into VORP Core, RSG Core or QBCore RedM at runtime. This guide is the whole path from ensure poggy_core to a working script, with the real verb names, taken from the poggy_core README and verb table.
What it is, and why
A RedM script that talks to the framework directly is written for that framework. VORP's inventory is callback-based, RSG's is synchronous, their notification signatures differ, their job-change events differ, and RSG's AddItem drops an item on the ground when the satchel is full while QBR does no capacity check at all. poggy_core hides all of that behind one shape: every call returns ok, value, err, capacity is checked before every add, Remove never takes a player negative, and a job change is one event on every framework.
Today it has three adapters, VORP Core, RSG Core and QBCore RedM, all proven in game; the QBR adapter is written against qbr-core and qbr-inventory and passes the full self-test. A script written against poggy_core runs on whichever adapters exist, and picks up new ones when poggy_core updates, without a change on your side. On a framework poggy_core has no adapter for, it says so and refuses framework calls rather than silently no-oping.
Install
Download Poggy Core (Free) (free, from Tebex at no charge), put the poggy_core folder in your resources, and start it after your framework and before anything that uses it:
ensure oxmysql
ensure vorp_core # or: ensure rsg-core / ensure qbr-core
ensure vorp_inventory # or: ensure rsg-inventory / ensure qbr-inventory
ensure poggy_core
# lets poggy_core restart the Poggy scripts it has just updated
add_ace resource.poggy_core command.refresh allow
add_ace resource.poggy_core command.ensure allow
There is no SQL to import and every value in poggy_core/config.lua has a working default. The two add_ace lines are for poggy_core's own updater; it prints them if they are missing. lua54 'yes' is set in poggy_core, and any resource that uses it should set it too.
Your first script
Two lines in the manifest do the work: load the bridge from poggy_core as your first shared script, and declare the dependency. The bridge is one file that lives in poggy_core, so updating poggy_core updates every script's copy of it.
-- fxmanifest.lua
fx_version 'cerulean'
game 'rdr3'
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'
lua54 'yes'
shared_script '@poggy_core/template/poggy.lua'
server_script 'server.lua'
client_script 'client.lua'
dependency 'poggy_core'
poggy_core_min '0.14.0' -- the oldest core this script will accept
Then one function, Poggy(verb, payload), for everything. A shop counter that takes money, checks the satchel and hands over an item, server side:
-- server.lua
RegisterNetEvent('myshop:buy', function(item, price)
local src = source
local ok, canCarry = Poggy('inv.canCarry', { src = src, item = item, qty = 1 })
if not ok or not canCarry then
return Poggy('notify', { src = src, text = 'Your satchel is full.', kind = 'error' })
end
local paid = Poggy('money.remove', { src = src, amount = price, currency = 'cash', reason = 'myshop:buy' })
if not paid then
return Poggy('notify', { src = src, text = 'You cannot afford that.', kind = 'error' })
end
Poggy('inv.add', { src = src, item = item, qty = 1 })
Poggy('notify', { src = src, text = 'Purchased.', kind = 'success' })
end)
Three things to notice. money.remove returns false, nil, 'no_funds' rather than taking the player negative, on every framework. inv.add capacity-checks first and returns false, nil, 'no_space' if the item would not fit, so the explicit inv.canCarry above is a courtesy message, not the safety net. And notify is drawn by poggy_core itself with the same RDR2 natives VORP uses, so nothing looks different and no framework notify resource is needed. kind is info, success, error or warning; notify.styled gives you the RDR2 banner styles (tip, objective, top, left, center and the rest) when position matters.
The same API is on an object if you prefer method calls: local Core = exports.poggy_core:Get() gives you Core.Money.Remove(src, 'cash', price, reason), Core.Inventory.Add(src, item, 1), Core.Notify(src, text, kind) and so on, with the same ok, err contract. Both go through the same dispatcher.
Branch on ok, and nothing else
Every verb returns ok, value, err. A core that is not ready yet, or one with no framework under it, answers false, nil, 'no_core', which looks exactly like any other failed call, so there is only one thing to handle:
local ok, char = Poggy('char.get', { src = src })
if not ok then return end
print(char.fullName, char.job, char.jobGrade, char.onDuty) -- onDuty may be nil: the framework cannot say
The error codes are a fixed set: not_ready, no_char, unsupported, not_implemented, bad_argument, no_space, no_funds, not_found, timeout, framework_error, needs_thread, plus no_core and core_too_old from the bridge. Work that must run at startup, such as registering storage, waits with PoggyReady() first.
Menus and inputs
Since 0.14.0 poggy_core draws its own list menu and text box, so your script needs neither vorp_menu nor vorp_inputs and gets the same screens on every framework. The two verbs that wait for the player need a thread (a CreateThread, an event handler, or a callback). On the client:
-- client.lua
CreateThread(function()
local ok, pick = Poggy('menu.open', {
title = 'Stable',
subtitle = 'Pick a horse',
items = {
{ label = 'Arabian', value = 'horse_arabian', right = '$120', desc = 'Fast and nervous.' },
{ label = 'Shire', value = 'horse_shire', right = '$80', desc = 'Strong and steady.' },
{ label = 'Mustang', value = 'horse_mustang', right = 'sold', disabled = true },
},
})
if not ok then return end -- 'closed': the player backed out
print(pick.value, pick.index, pick.item.label)
local okName, name = Poggy('input.text', { title = 'Name your horse', placeholder = 'Buttercup', maxLength = 24 })
if okName then print('named ' .. name) end
local okQty, qty = Poggy('input.text', { title = 'How many?', default = 1, numeric = true })
if okQty then print(qty + 1) end -- a number, not a string
end)
Poggy('menu.close', {}) -- take down whatever is open
On the server, name the player with src and the same verbs ride poggy_core's callback transport to that client and back; the answer is the client's answer. Because the items cross the network, a value must be plain data, not a function. One menu or input at a time: opening another replaces it, and the earlier caller gets false, 'closed'. Position for every Poggy menu on the server is one setting, PoggyCoreConfig.Ui.Position.
RegisterNetEvent('mystable:browse', function()
local src = source -- an event handler is a thread already
local ok, pick = Poggy('menu.open', { src = src, title = 'Stable', items = horsesFor(src) })
if not ok then return end
if not Poggy('money.remove', { src = src, amount = pick.item.price }) then
return Poggy('notify', { src = src, text = 'You cannot afford that.', kind = 'error' })
end
Poggy('notify', { src = src, text = 'Bought ' .. pick.item.label, kind = 'success' })
end)
Storage containers
A container is a stash in the framework's own inventory (a vorp_inventory custom inventory, an rsg-inventory stash), registered by id. Register on every boot: VORP keeps definitions in memory only, RSG persists the contents but not the label and size, and poggy_core replays your registrations if the inventory resource restarts underneath you.
-- server.lua
CreateThread(function()
if not PoggyReady() then return end
Poggy('storage.register', { id = 'town_safe', opts = {
label = 'Town Safe', slots = 60, maxWeight = 200,
jobAccess = { sheriff = 1 }, -- sheriff grade 1 and up
} })
end)
RegisterNetEvent('mysafe:open', function()
local src = source
local ok, _, err = Poggy('storage.open', { src = src, id = 'town_safe' })
if not ok then Poggy('notify', { src = src, text = 'Cannot open the safe (' .. tostring(err) .. ').', kind = 'error' }) end
end)
Ids are namespaced per script as pg_<your resource>_<id>, so two scripts cannot collide and a renamed folder does not hide what was stored; pass raw = true if you already have containers in the wild under bare ids. maxWeight is ignored on VORP (slots limit there) and jobAccess has no counterpart on RSG; pass both and the script works on every framework. storage.addItem, storage.removeItem, storage.items, storage.unregister (forget it, keep the items) and storage.delete (destroy it and the items) complete the set. On VORP, storage.addItem needs a charId.
Jobs and duty
-- server side; the same verbs work on the client without src
local ok, job = Poggy('job.get', { src = src }) -- { name, grade, label, gradeLabel, onDuty }
local isLaw = select(2, Poggy('job.isLaw', { src = src }))
local canOpen = select(2, Poggy('job.has', { src = src, job = { 'sheriff', 'marshal' }, minGrade = 1 }))
Poggy('job.set', { src = src, job = 'sheriff', grade = 2, persist = true }) -- one call on every framework
Poggy('job.duty', { src = src, onDuty = true }) -- RSG yes; VORP refuses: 'unsupported'
local _, deputies = Poggy('players.onDuty', { job = 'sheriff', minGrade = 1 }) -- server ids, never a guess
Which jobs count as law or medical is the server owner's setting in poggy_core/config.lua, not something your script decides, so job.isLaw agrees with every other script on the server. onDuty is nil when the framework cannot say (VORP without vorp_police running), a boolean on RSG where duty is part of the job. job.set with persist = true also writes the framework's own job columns so resources that read the database see it at once. Job changes arrive as one event however the framework signals them; on the client, listen for poggy_core:jobChangedLocal.
Running on a server with no framework
poggy_core probes for a framework at start. When it finds none, or finds one it has no adapter for (anything other than VORP Core, RSG Core and QBCore RedM), it falls back to the standalone adapter. Standalone is honest rather than helpful: every framework verb (character, money, jobs, items, storage, permissions) refuses with unsupported, players.onDuty returns an empty list, and Core.GetFramework() reports 'standalone'. Nothing pretends to succeed.
What still works without a framework: notifications, menus and text inputs (poggy_core draws them itself), prompts, callbacks, and the SQL installer. So a script that needs none of the framework can run anywhere poggy_core runs, and a script that does can say so cleanly:
local Core = exports.poggy_core:Get()
Core.Ready(function()
if not Core.HasAdapter() then
print(('[myshop] poggy_core found %s but has no adapter for it; shop disabled'):format(Core.GetFramework()))
return
end
if not Core.Has('money.cash') then return end -- capabilities, not framework names
startShop()
end)
Core.HasAdapter() is false both when there is no framework and when there is one poggy_core has no adapter for. Check it before relying on framework calls, and check Core.Has(capability) (money.gold, job.duty, storage.permissions, char.offline and the rest) rather than branching on the framework's name; Core.GetFramework() is for logging.
Database, config and updates
If your resource has a sql/install.sql, the bridge runs it through poggy_core the first time PoggyReady() passes on the server, before your first query, on MariaDB and MySQL 8. CREATE TABLE IF NOT EXISTS, ALTER TABLE ... ADD COLUMN, named indexes, INSERT IGNORE and repeatable UPDATE ... WHERE are applied only when missing, so a normal start sends no DDL; anything that cannot repeat safely goes in sql/migrations/001.sql, run once and recorded. Server owners import nothing.
poggy_core's updater only touches scripts on its own feed. Your resource is not on it, so it is never written to; the config-merge behaviour you see on Poggy scripts (added settings added, removed settings cleaned up, the owner's values kept, originals backed up) is part of that updater rather than something your script gets on its own.
Diagnostics
From the server console, or in game for admins:
poggycore framework, version, capabilities, registered containers
poggycore test read-only smoke test against your own character
poggycore caps the full capability map
poggycore scripts registered scripts: id, folder (when different), version
poggycore usables usable items registered through poggy_core: item -> script
poggycore verbs every verb, its side and its payload keys
The smoke test modifies nothing. Every script that loads the bridge prints one boot line saying whether poggy_core is ready and which framework was found, so a customer's console reads as one product. If your script needs a verb added in a later poggy_core, declare poggy_core_min in the manifest: against an older running core the bridge prints one red line at boot and every call returns core_too_old rather than half-working.
Adding your own adapter
One file. Copy server/adapters/standalone.lua, which is the interface specification, fill in every method it lists, declare a caps table, and add the detection probe to shared/sh_detect.lua. The dispatcher calls those method names and nothing else, so an adapter cannot silently miss one, and poggycore caps shows what you declared. server/adapters/vorp.lua is the worked example with the framework traps listed at the top; server/adapters/rsg.lua is the second, written the same way with the rsg-core line numbers each trap was read from, and server/adapters/qbr.lua is the third. That is how QBCore RedM arrived: one adapter file, no change to any script.
Where to go next
The README inside the poggy_core folder is the full reference: every verb with its payload and return value, the capability list, the per-framework notes for money, jobs, inventory and storage, and the notification styles. Poggy Skillcheck (Free) is a second free, standalone building block (one client export for a skillcheck minigame), and Poggy Util (Free) shows a whole resource written against poggy_core, config and translations open. Questions go to the Rosewood Ridge Discord; bring the boot line.