NextGenFiveM DocsNextGenFiveM Docs

NextGen FiveM
Documentation

crafting

Guides

How It WorksInstallationEditor
CommandsFaq

Developer

Custom FrameworkCustom InventoryExportsHooksScript Explorer

Hooks

Learn how to use the hooks system to customize and control the crafting flow.

Previous

Exports

Next

Script Explorer

On this page

Hooks
Why hooks?
Available hook points
Transform hooks (can manipulate data)
Event hooks (fire-and-forget)
Context objects
crafting:canSeeBench
crafting:canInteract, crafting:enter, crafting:exit
crafting:getCategories
crafting:getCategoryRecipes
crafting:getRecipe
crafting:preCraft
crafting:postCraft
Register hooks
1) Register with a function (recommended)
2) Register via export (if you want to reuse the same callback)
Unregister
Return values
Transform hooks (canInteract, enter, exit, getCategories, getCategoryRecipes, preCraft)
Event hooks (postCraft)
Examples
Reduce crafting time based on player level
Modify ingredients based on player perks
Hide categories for specific players
Modify recipes per player (e.g., different crafting times)
Modify single recipe when opened
Hide bench from specific players
Max quantity per craft
Log after crafting
Advanced
Manual run/transform/emit from other resources
Hook execution order
Deep merging behavior
Notes
Best practices

Hooks

This resource exposes a powerful hooks system that lets other resources influence, manipulate, or react to the crafting flow. Hooks can be used to veto operations, modify data, or trigger side effects.

Why hooks?

  • Pre-crafting control: deny or allow crafting based on your own rules (whitelists, jobs, cooldowns, zones, etc.)
  • Data manipulation: modify recipe properties, crafting times, ingredients, categories, and more for specific players
  • Post-crafting reactions: logging, analytics, trigger other systems
  • Complete customization: customize entire benches, categories, and recipes per player

Available hook points

Transform hooks (can manipulate data)

These hooks allow you to modify the data that gets used in the crafting system. Return a table with the properties you want to change, and they will be merged into the context.

  • crafting:canSeeBench - Runs when a player enters proximity of a bench (before they can see it)

    • Can veto: return false or {success = false, message = 'key'} to hide the bench from the player
    • This hook runs in the proximity system, so if it returns false, the player will never see the bench
  • crafting:canInteract - Runs when checking if a player can interact with a bench

    • Can manipulate: benchType
    • Can veto: return false or {success = false, message = 'key'}
  • crafting:enter - Runs when a player enters a crafting bench

    • Can manipulate: benchType
    • Can veto: return false or {success = false, message = 'key'}
  • crafting:exit - Runs when a player exits a crafting bench

    • Can manipulate: benchType
    • Can veto: return false or {success = false, message = 'key'}
  • crafting:getCategories - Runs when fetching categories for a player

    • Can manipulate: categories (array of category objects)
    • Can veto: return false or {success = false, message = 'key'}
  • crafting:getCategoryRecipes - Runs when fetching recipes for a category

    • Can manipulate: recipes (array of recipe objects), totalRecipes, hasMore, nextPage
    • Can veto: return false or {success = false, message = 'key'}
  • crafting:getRecipe - Runs when fetching a single recipe for a player

    • Can manipulate: recipe (all properties like craftingTime, ingredients, results, etc.)
    • Can veto: return false or {success = false, message = 'key'}
  • crafting:preCraft - Runs right before a craft is accepted

    • Can manipulate: recipe (all properties like craftingTime, ingredients, results, etc.), quantity
    • Can veto: return false or {success = false, message = 'key'}

Event hooks (fire-and-forget)

  • crafting:postCraft (emit, fire-and-forget)
    • Runs after the craft has been queued/saved. Return values are ignored.

Context objects

Each hook receives a context table with relevant data. Here's what's available in each hook:

crafting:canSeeBench

{
    src = number,              -- player's source ID
    playerId = string,         -- persistent player identifier
    benchType = table,         -- bench type object (uuid, name, fullAccess, etc.)
    benchLocation = string,    -- location UUID
    distance = number,         -- distance from player to bench
    coords = vector3           -- player's current coordinates
}

crafting:canInteract, crafting:enter, crafting:exit

{
    src = number,              -- player's source ID
    playerId = string,         -- persistent player identifier
    benchType = table,         -- bench type object (uuid, name, fullAccess, etc.)
    benchLocation = string      -- location UUID
}

crafting:getCategories

{
    src = number,              -- player's source ID
    playerId = string,         -- persistent player identifier
    benchType = table,         -- bench type object
    benchLocation = string,    -- location UUID
    categories = table          -- array of category objects
}

crafting:getCategoryRecipes

{
    src = number,              -- player's source ID
    playerId = string,         -- persistent player identifier
    benchType = table,         -- bench type object
    benchLocation = string,    -- location UUID
    categoryId = string,       -- category UUID
    recipes = table,           -- array of recipe objects (paginated)
    allRecipes = table,        -- array of all recipe objects (before pagination)
    page = number,            -- current page number
    limit = number             -- items per page
}

crafting:getRecipe

{
    src = number,              -- player's source ID
    playerId = string,         -- persistent player identifier
    benchType = table,         -- bench type object
    benchLocation = string,    -- location UUID
    recipe = table             -- recipe object (uuid, title, ingredients, results, craftingTime, cooldown, etc.)
}

crafting:preCraft

{
    src = number,              -- player's source ID
    playerId = string,         -- persistent player identifier
    recipe = table,            -- recipe object (uuid, title, ingredients, results, craftingTime, etc.)
    quantity = number,         -- amount to craft
    benchType = table,         -- bench type object
    benchLocation = string,    -- location UUID
    inventory = table          -- player's item summary (itemName => count)
}

crafting:postCraft

{
    src = number,              -- player's source ID
    playerId = string,         -- persistent player identifier
    recipe = table,            -- recipe object
    quantity = number,         -- amount crafted
    craftId = string,          -- unique craft ID
    benchType = table,         -- bench type object
    benchLocation = string     -- location UUID
}

Register hooks

You can register either with an inline function or by pointing to an export in your resource.

1) Register with a function (recommended)

local id = exports['nextgenfivem_crafting']:registerHook('crafting:preCraft', {
    priority = 0, -- lower runs earlier
    fn = function(ctx)
        -- Return false to veto
        if ctx.quantity > 5 then
            return false, 'errors.invalid_quantity'
        end
 
        -- Return a table to manipulate data
        return {
            recipe = {
                craftingTime = ctx.recipe.craftingTime * 0.5 -- 50% faster
            }
        }
    end
})

2) Register via export (if you want to reuse the same callback)

In your resource (fxmanifest): export a function, e.g. myPreCraftCheck.

-- In your fxmanifest.lua
exports {
    'myPreCraftCheck'
}
 
-- In your server file
function myPreCraftCheck(ctx)
    -- Your logic here
    return true
end
 
-- Register hook by export name
exports['nextgenfivem_crafting']:registerHook('crafting:preCraft', {
    priority = 10,
    resource = GetCurrentResourceName(),
    export = 'myPreCraftCheck'
})

Unregister

exports['nextgenfivem_crafting']:unregisterHook('crafting:preCraft', id)

Return values

Transform hooks (canInteract, enter, exit, getCategories, getCategoryRecipes, preCraft)

A handler may return any of the following:

  • true or nil → allow and continue to the next handler (no changes)
  • false → stop operation, use default message crafting.not_authorized
  • { success = false, message = 'errors.key' } → stop operation with the given translation key
  • false, 'errors.key' → stop operation with the given translation key
  • Table with modifications → merge changes into context and continue

When returning a table for manipulation, you can modify nested properties. The system uses deep merging, so you only need to specify the properties you want to change:

-- Modify only craftingTime, other recipe properties remain unchanged
return {
    recipe = {
        craftingTime = 10
    }
}
 
-- Modify nested recipe properties
return {
    recipe = {
        ingredients = {
            { item = 'iron', count = 2 } -- replaces entire ingredients array
        },
        craftingTime = 5
    }
}

Event hooks (postCraft)

Return values are ignored. These hooks are fire-and-forget.


Examples

Reduce crafting time based on player level

exports['nextgenfivem_crafting']:registerHook('crafting:preCraft', {
    priority = 0,
    fn = function(ctx)
        -- Get player level from your system
        local playerLevel = exports['my_leveling']:getPlayerLevel(ctx.playerId)
 
        -- Calculate reduction (e.g., 5% per level, max 50% reduction)
        local reduction = math.min(0.5, playerLevel * 0.05)
 
        -- Return modified recipe
        return {
            recipe = {
                craftingTime = math.max(1, ctx.recipe.craftingTime * (1 - reduction))
            }
        }
    end
})

Modify ingredients based on player perks

exports['nextgenfivem_crafting']:registerHook('crafting:preCraft', {
    priority = 0,
    fn = function(ctx)
        local hasPerk = exports['my_perks']:hasPerk(ctx.playerId, 'efficient_crafter')
 
        if hasPerk then
            -- Reduce ingredient requirements by 20%
            local modifiedIngredients = {}
            for _, ingredient in ipairs(ctx.recipe.ingredients) do
                table.insert(modifiedIngredients, {
                    item = ingredient.item,
                    count = math.max(1, math.floor(ingredient.count * 0.8))
                })
            end
 
            return {
                recipe = {
                    ingredients = modifiedIngredients
                }
            }
        end
 
        return true
    end
})

Hide categories for specific players

exports['nextgenfivem_crafting']:registerHook('crafting:getCategories', {
    priority = 0,
    fn = function(ctx)
        local playerRank = exports['my_framework']:getPlayerRank(ctx.src)
 
        -- Hide premium categories for non-premium players
        if playerRank < 5 then
            local filteredCategories = {}
            for _, category in ipairs(ctx.categories) do
                if not category.isPremium then
                    table.insert(filteredCategories, category)
                end
            end
 
            return {
                categories = filteredCategories
            }
        end
 
        return true
    end
})

Modify recipes per player (e.g., different crafting times)

exports['nextgenfivem_crafting']:registerHook('crafting:getCategoryRecipes', {
    priority = 0,
    fn = function(ctx)
        local playerLevel = exports['my_leveling']:getPlayerLevel(ctx.playerId)
 
        -- Modify each recipe's crafting time based on player level
        local modifiedRecipes = {}
        for _, recipe in ipairs(ctx.recipes) do
            local reduction = math.min(0.3, playerLevel * 0.02) -- 2% per level, max 30%
            local newRecipe = table.clone(recipe, true)
            newRecipe.craftingTime = math.max(1, recipe.craftingTime * (1 - reduction))
            table.insert(modifiedRecipes, newRecipe)
        end
 
        return {
            recipes = modifiedRecipes
        }
    end
})

Modify single recipe when opened

exports['nextgenfivem_crafting']:registerHook('crafting:getRecipe', {
    priority = 0,
    fn = function(ctx)
        local playerLevel = exports['my_leveling']:getPlayerLevel(ctx.playerId)
 
        -- Reduce crafting time based on player level
        local reduction = math.min(0.5, playerLevel * 0.05) -- 5% per level, max 50%
 
        return {
            recipe = {
                craftingTime = math.max(1, ctx.recipe.craftingTime * (1 - reduction))
            }
        }
    end
})

Hide bench from specific players

exports['nextgenfivem_crafting']:registerHook('crafting:canSeeBench', {
    priority = 0,
    fn = function(ctx)
        local playerBanned = exports['my_system']:isPlayerBanned(ctx.playerId)
 
        -- Hide bench from banned players
        if playerBanned then
            return false
        end
 
        local playerLevel = exports['my_leveling']:getPlayerLevel(ctx.playerId)
 
        -- Hide premium benches from low-level players
        if ctx.benchType.isPremium and playerLevel < 10 then
            return false
        end
 
        return true
    end
})

Max quantity per craft

exports['nextgenfivem_crafting']:registerHook('crafting:preCraft', {
    priority = 0,
    fn = function(ctx)
        if ctx.quantity > 10 then
            return false, 'errors.invalid_quantity'
        end
        return true
    end
})

Log after crafting

exports['nextgenfivem_crafting']:registerHook('crafting:postCraft', {
    fn = function(ctx)
        print(('[Crafting] %s crafted %dx %s'):format(ctx.playerId, ctx.quantity, ctx.recipe.uuid))
        -- Send to your analytics system
        exports['my_analytics']:trackCraft(ctx.playerId, ctx.recipe.uuid, ctx.quantity)
    end
})

Advanced

Manual run/transform/emit from other resources

If you need to trigger the hook chain yourself:

-- run (with veto), returns ok, msg
local ok, msg = exports['nextgenfivem_crafting']:runHook('crafting:preCraft', ctx)
 
-- transform (with manipulation), returns ok, modifiedContext, msg
local ok, modifiedCtx, msg = exports['nextgenfivem_crafting']:transformHook('crafting:preCraft', ctx)
 
-- emit (ignores return values)
exports['nextgenfivem_crafting']:emitHook('crafting:postCraft', ctx)

Hook execution order

Hooks are executed in priority order (lower numbers run first). This is important when you have multiple hooks that might modify the same data:

-- This runs first (priority 0)
exports['nextgenfivem_crafting']:registerHook('crafting:preCraft', {
    priority = 0,
    fn = function(ctx)
        -- Reduce crafting time by 20%
        return {
            recipe = {
                craftingTime = ctx.recipe.craftingTime * 0.8
            }
        }
    end
})
 
-- This runs second (priority 10) and receives the already-modified recipe
exports['nextgenfivem_crafting']:registerHook('crafting:preCraft', {
    priority = 10,
    fn = function(ctx)
        -- Further reduce by 10% (applied to the already-reduced time)
        return {
            recipe = {
                craftingTime = ctx.recipe.craftingTime * 0.9
            }
        }
    end
})

Deep merging behavior

When you return a table from a transform hook, the system performs a deep merge. This means:

  • Top-level properties are merged
  • Nested tables are recursively merged
  • Arrays are replaced entirely (not merged)
-- Original recipe has: { craftingTime = 10, ingredients = {{item = 'iron', count = 5}} }
 
-- Hook returns:
return {
    recipe = {
        craftingTime = 5,  -- This replaces craftingTime
        ingredients = {{item = 'iron', count = 3}}  -- This REPLACES the entire ingredients array
    }
}
 
-- Final result: { craftingTime = 5, ingredients = {{item = 'iron', count = 3}} }

Notes

  • Handlers are automatically removed when the resource that registered them stops.
  • Priority: lower numbers run earlier. Default 0.
  • Keep handlers fast; avoid long synchronous operations in hooks.
  • If a handler errors, it is logged and the operation may fail with a generic error.
  • Transform hooks use deep merging, so you only need to specify the properties you want to change.
  • Arrays in nested objects are replaced entirely, not merged.
  • You can combine veto and manipulation: return false to veto, or return a table to modify data.
  • Multiple hooks can modify the same data; changes are applied in priority order.

Best practices

  1. Use appropriate priorities: Set lower priorities for hooks that should run first (e.g., access checks before modifications).

  2. Clone arrays when modifying: If you need to modify arrays in recipes, clone them first to avoid mutating the original:

local modifiedIngredients = {}
for _, ingredient in ipairs(ctx.recipe.ingredients) do
    table.insert(modifiedIngredients, {
        item = ingredient.item,
        count = ingredient.count * 0.8
    })
end
return { recipe = { ingredients = modifiedIngredients } }
  1. Validate modifications: Always validate that your modifications make sense (e.g., crafting time should be >= 1).

  2. Cache expensive operations: If you're fetching player data, consider caching it to avoid performance issues.

  3. Handle errors gracefully: Wrap external calls in pcall or check for nil values.