Skip to main content

What Are Meta Items?

Meta items are inventory items that store custom metadata (also called info data) beyond the basic item properties. This metadata allows items to have unique characteristics, track additional information, or store custom values that make each item instance different.
Meta items enable dynamic gameplay features like: quality tracking, serial numbers, stored values, timestamps, player names, custom descriptions, and much more!

Understanding Item Metadata

Basic Item Structure

Every inventory item has standard properties:
{
    name = 'bread',
    label = 'Bread',
    amount = 5,
    type = 'item',
    slot = 3,
    weight = 200,
    info = {},  -- This is where metadata lives!
    unique = false,
    useable = true,
    image = 'bread.png',
    description = 'Fresh bread'
}

The info Field

The info field is a Lua table that can store any custom data you want:
info = {
    quality = 85,
    craftedBy = 'John_Doe',
    timestamp = 1234567890,
    serialNumber = 'ABC123XYZ',
    customValue = 'anything',
    nested = {
        data = 'supported too!'
    }
}
The info table is saved to the database and persists across server restarts. It’s stored as JSON in the database.

Reserved Metadata Fields

Some metadata fields have special meaning in the RSG Framework:

1. quality - Item Condition

Used by the decay system and weapons:
info = {
    quality = 100  -- 0-100 scale
}
  • 100: Perfect condition
  • 75-99: Good condition
  • 50-74: Decent condition
  • 25-49: Poor condition
  • 0-24: Very poor condition
  • 0: Item is destroyed (deleted on next inventory load)

2. lastUpdate - Decay Timestamp

Automatically set for items with the decay property:
info = {
    quality = 100,
    lastUpdate = 1704067200  -- Unix timestamp
}
Never manually set lastUpdate unless you know what you’re doing! The system manages this automatically.

3. serie - Weapon Serial Number

Automatically generated for weapons:
info = {
    serie = '12ABC3XY456WXYZ',  -- Unique weapon identifier
    quality = 100,
    ammo = 6
}

4. ammo - Weapon Ammunition

Current ammo count for weapons:
info = {
    serie = 'ABC123',
    quality = 85,
    ammo = 6  -- Current loaded ammo
}

Creating Items with Metadata

Method 1: AddItem with Info Parameter

The primary way to add metadata is through the info parameter:
-- SERVER SIDE
exports['rsg-inventory']:AddItem(source, 'item_name', amount, slot, info, reason)
Parameters:
  • source - Player server ID or stash identifier
  • item_name - Name of the item (from shared/items.lua)
  • amount - Quantity to add
  • slot - (optional) Specific slot number, or nil for auto-slot
  • info - The metadata table
  • reason - Reason for logging

Example: Simple Meta Item

-- SERVER SIDE
local src = source
local Player = RSGCore.Functions.GetPlayer(src)
if not Player then return end

local info = {
    quality = 100,
    craftedBy = GetPlayerName(src),
    craftDate = os.date('%Y-%m-%d %H:%M:%S')
}

exports['rsg-inventory']:AddItem(src, 'bread', 1, nil, info, 'crafted-by-player')

lib.notify(src, {
    description = 'You crafted fresh bread!',
    type = 'success'
})

Example: Money Clip (Real Framework Example)

The money_clip item stores cash amount in metadata:
-- SERVER SIDE
RegisterNetEvent('rsg-banking:server:transact', function(type, amount)
    local src = source
    local Player = RSGCore.Functions.GetPlayer(src)
    if not Player then return end

    if type == 3 then -- Create money clip
        local currentBank = Player.Functions.GetMoney('bank')

        if currentBank >= amount then
            local info = {
                money = amount  -- Store the cash amount
            }

            Player.Functions.RemoveMoney('bank', amount, 'bank-money_clip')
            Player.Functions.AddItem('money_clip', 1, false, info)

            lib.notify(src, {
                description = 'Created money clip with $' .. amount,
                type = 'success'
            })
        end
    end
end)
Then when used:
-- SERVER SIDE
RSGCore.Functions.CreateUseableItem('money_clip', function(source, item)
    local src = source
    local Player = RSGCore.Functions.GetPlayer(src)
    if not Player then return end

    local moneyAmount = item.info.money or 0

    if moneyAmount > 0 then
        Player.Functions.RemoveItem('money_clip', 1, item.slot, 'money-clip-used')
        Player.Functions.AddMoney('cash', moneyAmount, 'money-clip-cashed')

        lib.notify(src, {
            description = 'You received $' .. moneyAmount .. ' from the money clip',
            type = 'success'
        })
    end
end)

Use Cases & Examples

Use Case 1: Crafted Items with Crafter Name

Track who crafted an item:
-- SERVER SIDE
RegisterNetEvent('crafting:server:craftItem', function(itemName)
    local src = source
    local Player = RSGCore.Functions.GetPlayer(src)
    if not Player then return end

    -- Check if player has materials (simplified)
    local hasMaterials = exports['rsg-inventory']:HasItem(src, 'wood', 10)

    if hasMaterials then
        exports['rsg-inventory']:RemoveItem(src, 'wood', 10, nil, 'crafting')

        local info = {
            quality = 100,
            craftedBy = Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname,
            craftDate = os.date('%B %d, %Y'),
            craftedCitizenId = Player.PlayerData.citizenid
        }

        exports['rsg-inventory']:AddItem(src, 'wooden_chair', 1, nil, info, 'crafted')

        lib.notify(src, {
            description = 'You crafted a wooden chair!',
            type = 'success'
        })
    end
end)
Now when players view the chair in their inventory, they can see who crafted it!

Use Case 2: Signed Documents

Create items that can be signed by players:
-- SERVER SIDE
RegisterNetEvent('documents:server:signDocument', function(slot)
    local src = source
    local Player = RSGCore.Functions.GetPlayer(src)
    if not Player then return end

    local document = exports['rsg-inventory']:GetItemBySlot(src, slot)

    if document and document.name == 'blank_document' then
        -- Remove unsigned document
        exports['rsg-inventory']:RemoveItem(src, 'blank_document', 1, slot, 'document-signed')

        -- Add signed document with metadata
        local info = {
            signedBy = Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname,
            signedDate = os.date('%B %d, %Y at %I:%M %p'),
            citizenId = Player.PlayerData.citizenid,
            documentNumber = 'DOC-' .. math.random(10000, 99999)
        }

        exports['rsg-inventory']:AddItem(src, 'signed_document', 1, slot, info, 'document-signed')

        lib.notify(src, {
            description = 'Document signed successfully',
            type = 'success'
        })
    end
end)

Use Case 3: Timed Items / Expiration

Create items that expire after a certain time:
-- SERVER SIDE
RegisterNetEvent('pharmacy:server:buyMedicine', function(medicineType)
    local src = source
    local Player = RSGCore.Functions.GetPlayer(src)
    if not Player then return end

    local price = 50
    local hasMoney = Player.Functions.GetMoney('cash') >= price

    if hasMoney then
        Player.Functions.RemoveMoney('cash', price, 'medicine-purchase')

        local info = {
            quality = 100,
            purchaseDate = os.time(),
            expiresAt = os.time() + (7 * 24 * 60 * 60), -- Expires in 7 days
            batchNumber = 'MED-' .. math.random(1000, 9999)
        }

        exports['rsg-inventory']:AddItem(src, 'medicine', 1, nil, info, 'pharmacy-purchase')

        lib.notify(src, {
            description = 'Medicine purchased (expires in 7 days)',
            type = 'success'
        })
    end
end)

-- Check expiration when used
RSGCore.Functions.CreateUseableItem('medicine', function(source, item)
    local src = source
    local Player = RSGCore.Functions.GetPlayer(src)
    if not Player then return end

    local currentTime = os.time()
    local expiresAt = item.info.expiresAt or currentTime

    if currentTime > expiresAt then
        lib.notify(src, {
            description = 'This medicine has expired!',
            type = 'error'
        })

        -- Optionally remove expired medicine
        exports['rsg-inventory']:RemoveItem(src, 'medicine', 1, item.slot, 'expired')
        return
    end

    -- Medicine is still good, use it
    exports['rsg-inventory']:RemoveItem(src, 'medicine', 1, item.slot, 'consumed')

    -- Heal player
    TriggerClientEvent('rsg-medical:client:heal', src, 50)

    lib.notify(src, {
        description = 'You took medicine and feel better',
        type = 'success'
    })
end)

Use Case 4: Container Items (Bags/Pouches)

Items that store other items:
-- SERVER SIDE
RegisterNetEvent('containers:server:openPouch', function(slot)
    local src = source
    local Player = RSGCore.Functions.GetPlayer(src)
    if not Player then return end

    local pouch = exports['rsg-inventory']:GetItemBySlot(src, slot)

    if pouch and pouch.name == 'coin_pouch' then
        local coins = pouch.info.coins or 0

        lib.alertDialog({
            header = 'Coin Pouch',
            content = 'This pouch contains ' .. coins .. ' gold coins.\n\nWhat would you like to do?',
            centered = true,
            cancel = true,
            labels = {
                confirm = 'Take Coins',
                cancel = 'Close'
            }
        }, function(confirmed)
            if confirmed then
                -- Remove pouch
                exports['rsg-inventory']:RemoveItem(src, 'coin_pouch', 1, slot, 'pouch-opened')

                -- Give coins
                Player.Functions.AddMoney('cash', coins, 'pouch-coins')

                lib.notify(src, {
                    description = 'You took ' .. coins .. ' coins from the pouch',
                    type = 'success'
                })
            end
        end)
    end
end)

-- Creating pouches with random amounts
RegisterNetEvent('loot:server:generatePouch', function()
    local src = source
    local randomCoins = math.random(50, 500)

    local info = {
        coins = randomCoins,
        foundBy = GetPlayerName(src),
        foundDate = os.date('%Y-%m-%d')
    }

    exports['rsg-inventory']:AddItem(src, 'coin_pouch', 1, nil, info, 'loot-found')
end)

Use Case 5: Custom Description Items

Items with player-written descriptions:
-- SERVER SIDE
RegisterNetEvent('items:server:writeNote', function(slot, noteText)
    local src = source
    local Player = RSGCore.Functions.GetPlayer(src)
    if not Player then return end

    local paper = exports['rsg-inventory']:GetItemBySlot(src, slot)

    if paper and paper.name == 'blank_paper' then
        if #noteText > 500 then
            lib.notify(src, {
                description = 'Note is too long (max 500 characters)',
                type = 'error'
            })
            return
        end

        -- Remove blank paper
        exports['rsg-inventory']:RemoveItem(src, 'blank_paper', 1, slot, 'note-written')

        -- Add written note with custom text
        local info = {
            noteText = noteText,
            writtenBy = Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname,
            writtenDate = os.date('%B %d, %Y'),
            noteId = 'NOTE-' .. RSGCore.Shared.RandomStr(8)
        }

        exports['rsg-inventory']:AddItem(src, 'written_note', 1, slot, info, 'note-written')

        lib.notify(src, {
            description = 'Note written successfully',
            type = 'success'
        })
    end
end)

-- Reading notes
RSGCore.Functions.CreateUseableItem('written_note', function(source, item)
    local src = source
    local noteText = item.info.noteText or 'This note is blank.'
    local writtenBy = item.info.writtenBy or 'Unknown'
    local writtenDate = item.info.writtenDate or 'Unknown date'

    lib.alertDialog({
        header = 'Written Note',
        content = noteText .. '\n\n~ ' .. writtenBy .. '\n' .. writtenDate,
        centered = true,
        cancel = false
    })
end)

Use Case 6: Pet Items with Stats

Items representing pets with custom stats:
-- SERVER SIDE
RegisterNetEvent('pets:server:adoptPet', function(petType)
    local src = source
    local Player = RSGCore.Functions.GetPlayer(src)
    if not Player then return end

    local petNames = {'Max', 'Buddy', 'Charlie', 'Rocky', 'Duke', 'Bear', 'Scout'}
    local randomName = petNames[math.random(#petNames)]

    local info = {
        petName = randomName,
        petType = petType,
        hunger = 100,
        happiness = 100,
        health = 100,
        level = 1,
        adoptedBy = Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname,
        adoptedDate = os.date('%B %d, %Y'),
        petId = 'PET-' .. RSGCore.Shared.RandomStr(10)
    }

    exports['rsg-inventory']:AddItem(src, 'pet_whistle', 1, nil, info, 'pet-adopted')

    lib.notify(src, {
        description = 'You adopted ' .. randomName .. ' the ' .. petType .. '!',
        type = 'success'
    })
end)

-- Using pet whistle to check stats
RSGCore.Functions.CreateUseableItem('pet_whistle', function(source, item)
    local src = source
    local petInfo = item.info

    local content = string.format(
        'Name: %s\nType: %s\n\nHunger: %d%%\nHappiness: %d%%\nHealth: %d%%\nLevel: %d\n\nAdopted: %s',
        petInfo.petName or 'Unknown',
        petInfo.petType or 'Unknown',
        petInfo.hunger or 0,
        petInfo.happiness or 0,
        petInfo.health or 0,
        petInfo.level or 1,
        petInfo.adoptedDate or 'Unknown'
    )

    lib.alertDialog({
        header = 'Pet Stats - ' .. (petInfo.petName or 'Pet'),
        content = content,
        centered = true,
        cancel = false
    })
end)

Accessing Item Metadata

Server-Side Access

Get Item by Slot

local item = exports['rsg-inventory']:GetItemBySlot(source, slotNumber)

if item then
    print('Item name:', item.name)
    print('Item amount:', item.amount)
    print('Item metadata:', json.encode(item.info))

    -- Access specific metadata
    local quality = item.info.quality
    local craftedBy = item.info.craftedBy
end

Get Item by Name

local item = exports['rsg-inventory']:GetItemByName(source, 'money_clip')

if item then
    local moneyAmount = item.info.money or 0
    print('Money clip contains: $' .. moneyAmount)
end

Get All Items

local Player = RSGCore.Functions.GetPlayer(source)
if not Player then return end

for slot, item in pairs(Player.PlayerData.items) do
    if item and item.name == 'signed_document' then
        print('Document signed by:', item.info.signedBy)
        print('Document date:', item.info.signedDate)
    end
end

Client-Side Access

Access metadata through PlayerData:
-- CLIENT SIDE
local RSGCore = exports['rsg-core']:GetCoreObject()
local PlayerData = RSGCore.Functions.GetPlayerData()

for slot, item in pairs(PlayerData.items) do
    if item and item.name == 'wooden_chair' then
        print('Chair crafted by:', item.info.craftedBy)
    end
end
Or through callbacks:
-- CLIENT SIDE
RSGCore.Functions.TriggerCallback('inventory:server:getItemInfo', function(item)
    if item then
        print('Item metadata:', json.encode(item.info))
    end
end, slotNumber)

Modifying Existing Metadata

Method 1: SetItemData

-- SERVER SIDE
exports['rsg-inventory']:SetItemData(source, 'medicine', 'info', {
    quality = 85,
    expiresAt = os.time() + (5 * 24 * 60 * 60)
})

Method 2: Remove and Re-Add

For more complex updates, remove and re-add the item:
-- SERVER SIDE
local item = exports['rsg-inventory']:GetItemByName(source, 'pet_whistle')

if item then
    local oldInfo = item.info
    local slot = item.slot

    -- Update metadata
    oldInfo.hunger = math.max(0, oldInfo.hunger - 10)
    oldInfo.happiness = math.max(0, oldInfo.happiness - 5)

    -- Remove old item
    exports['rsg-inventory']:RemoveItem(source, 'pet_whistle', 1, slot, 'pet-updated')

    -- Add updated item
    exports['rsg-inventory']:AddItem(source, 'pet_whistle', 1, slot, oldInfo, 'pet-updated')
end

Best Practices

Keep metadata small: Only store essential data. Large metadata increases database size and network traffic.
Never trust client data: Always validate metadata on server-side before accepting it.
Use consistent keys: Establish naming conventions for your metadata fields (e.g., always use craftedBy instead of mixing crafter, made_by, etc.)

✅ Good Practices

-- Good: Small, efficient metadata
local info = {
    quality = 100,
    craftedBy = playerName,
    timestamp = os.time()
}

-- Good: Use numbers for timestamps (smaller than date strings)
local info = {
    createdAt = os.time()  -- Unix timestamp
}

-- Good: Short identifiers
local info = {
    id = 'DOC-12345'
}

❌ Bad Practices

-- Bad: Storing huge text
local info = {
    description = string.rep('Very long text...', 1000)  -- Too large!
}

-- Bad: Redundant data already in item definition
local info = {
    name = 'bread',  -- Already in item.name
    label = 'Bread',  -- Already in item.label
    weight = 200  -- Already in item.weight
}

-- Bad: Storing entire player object
local info = {
    player = Player.PlayerData  -- Way too much data!
}

Performance Tips

  1. Limit metadata size: Keep under 1KB per item when possible
  2. Index frequently accessed fields: If you query specific metadata often, consider separate database tables
  3. Clean up old metadata: Periodically remove expired or unused metadata
  4. Validate inputs: Check metadata before adding to prevent bad data

Unique Items vs Non-Unique Items

Understanding unique Property

Items can be marked as unique = true in shared/items.lua:
-- Non-unique item (can stack)
bread = {
    name = 'bread',
    label = 'Bread',
    weight = 100,
    type = 'item',
    image = 'bread.png',
    unique = false  -- Can stack
}

-- Unique item (cannot stack)
money_clip = {
    name = 'money_clip',
    label = 'Money Clip',
    weight = 1,
    type = 'item',
    image = 'money_clip.png',
    unique = true  -- Each takes its own slot
}

Stacking Behavior with Metadata

Non-Unique Items:
  • Only stack if metadata (info) is identical
  • Different metadata = different stacks
-- These will stack together (same metadata)
AddItem(source, 'bread', 5, nil, {quality = 100}, 'crafted')
AddItem(source, 'bread', 5, nil, {quality = 100}, 'crafted')
-- Result: 10x Bread in one slot

-- These will NOT stack (different metadata)
AddItem(source, 'bread', 5, nil, {quality = 100}, 'crafted')
AddItem(source, 'bread', 5, nil, {quality = 50}, 'crafted')
-- Result: Two separate stacks in different slots
Unique Items:
  • Always take individual slots
  • Even if metadata is identical
-- Each money clip gets its own slot
AddItem(source, 'money_clip', 1, nil, {money = 100}, 'created')
AddItem(source, 'money_clip', 1, nil, {money = 100}, 'created')
-- Result: Two separate slots with 1x Money Clip each
When creating items with varying metadata, consider making them unique = true to prevent unexpected stacking behavior.

Decay System Integration

Items with the decay property automatically use metadata for quality tracking:
-- In shared/items.lua
apple = {
    name = 'apple',
    label = 'Apple',
    weight = 100,
    type = 'item',
    image = 'apple.png',
    unique = false,
    useable = true,
    decay = 120,  -- Decay time in minutes (2 hours)
    delete = true,  -- Delete when quality reaches 0
    shouldClose = true,
    description = 'Fresh apple'
}
When added, the system automatically sets:
info = {
    quality = 100,  -- Starts at 100%
    lastUpdate = 1704067200  -- Current timestamp
}
Over time, quality decreases based on the decay rate.
Items with 0% quality are automatically deleted when the inventory is loaded if delete = true!

Database Storage

Metadata is stored as JSON in the database:

Player Inventory

SELECT inventory FROM players WHERE citizenid = 'ABC12345';
Result:
[
    {
        "name": "money_clip",
        "amount": 1,
        "info": {
            "money": 500
        },
        "type": "item",
        "slot": 1
    },
    {
        "name": "signed_document",
        "amount": 1,
        "info": {
            "signedBy": "John Doe",
            "signedDate": "January 15, 2025",
            "documentNumber": "DOC-54321"
        },
        "type": "item",
        "slot": 2
    }
]

Stash Storage

SELECT items FROM inventories WHERE identifier = 'house_123';
Metadata is stored the same way for stashes.

Common Errors & Troubleshooting

Error: Metadata Not Persisting

Problem: Item metadata disappears after server restart Cause: Item not saved properly Solution: Ensure the item is added correctly and the inventory is being saved:
-- Make sure AddItem returns true
local success = exports['rsg-inventory']:AddItem(source, 'item', 1, nil, info, 'reason')

if not success then
    print('Failed to add item!')
end

-- For stashes, make sure you've created the inventory first
exports['rsg-inventory']:CreateInventory('stash_id', {
    label = 'Stash Name',
    maxweight = 50000,
    slots = 25
})

Error: Metadata Overwriting

Problem: Metadata gets overwritten when adding more of the same item Cause: Non-unique items with identical metadata stack together Solution: Either:
  1. Make the item unique = true in items.lua
  2. Ensure each item has different metadata
  3. Use a specific slot to prevent stacking
-- Force new slot by not specifying slot
AddItem(source, 'item', 1, nil, {id = 'unique-' .. math.random(1000, 9999)}, 'reason')

Error: Accessing Nil Metadata

Problem: Trying to access metadata that doesn’t exist
local item = GetItemByName(source, 'bread')
local craftedBy = item.info.craftedBy  -- Error if info.craftedBy doesn't exist!
Solution: Always check if metadata exists:
local item = exports['rsg-inventory']:GetItemByName(source, 'bread')

if item then
    local craftedBy = item.info and item.info.craftedBy or 'Unknown'
    print('Crafted by:', craftedBy)
end

Advanced: Custom Metadata Validation

Create a validation system for metadata:
-- SERVER SIDE
local MetadataSchemas = {
    money_clip = {
        required = {'money'},
        types = {
            money = 'number'
        },
        validate = function(info)
            return info.money and info.money > 0 and info.money <= 10000
        end
    },
    signed_document = {
        required = {'signedBy', 'documentNumber'},
        types = {
            signedBy = 'string',
            documentNumber = 'string'
        }
    }
}

function ValidateItemMetadata(itemName, info)
    local schema = MetadataSchemas[itemName]
    if not schema then return true end  -- No validation needed

    -- Check required fields
    for _, field in ipairs(schema.required or {}) do
        if not info[field] then
            return false, 'Missing required field: ' .. field
        end
    end

    -- Check field types
    for field, expectedType in pairs(schema.types or {}) do
        if info[field] and type(info[field]) ~= expectedType then
            return false, 'Invalid type for field ' .. field .. ': expected ' .. expectedType
        end
    end

    -- Custom validation
    if schema.validate and not schema.validate(info) then
        return false, 'Custom validation failed'
    end

    return true
end

-- Usage
RegisterNetEvent('banking:server:createMoneyClip', function(amount)
    local src = source
    local Player = RSGCore.Functions.GetPlayer(src)
    if not Player then return end

    local info = {
        money = amount
    }

    local valid, error = ValidateItemMetadata('money_clip', info)

    if not valid then
        lib.notify(src, {
            description = 'Invalid money clip: ' .. error,
            type = 'error'
        })
        return
    end

    -- Proceed with creating money clip
    exports['rsg-inventory']:AddItem(src, 'money_clip', 1, nil, info, 'bank-withdraw')
end)

Summary

Meta items provide powerful customization for inventory items:
FeatureDescription
Custom DataStore any data in the info table
PersistenceMetadata is saved to database
Reserved Fieldsquality, lastUpdate, serie, ammo have special meanings
StackingNon-unique items only stack if metadata is identical
AccessUse GetItemBySlot/GetItemByName to retrieve metadata
ValidationAlways validate metadata server-side

Next Steps


Need more help? Join the RSG Framework Discord!