Skip to content

Client Hooks ​

Client hooks allow you to react to events on the client side. Use these to trigger custom UI effects, notifications, animations, sounds, particle FX, or any client-side logic when specific crafting events occur.

How to Use Hooks ​

Hooks in sd-crafting use a callback registration system, not standard FiveM events. Register handlers using either:

From within the sd-crafting resource (in client/hooks.lua):

lua
ClientHooks.Register('onCraftingUIOpened', function(data)
    print(('Opened crafting UI at: %s'):format(data.stationId))
end)

From another resource (via export):

lua
exports['sd-crafting']:registerClientHook('onCraftingUIOpened', function(data)
    print(('Opened crafting UI at: %s'):format(data.stationId))
end)

Both methods return a handlerId that can be used to unregister the hook later:

lua
local id = exports['sd-crafting']:registerClientHook('onCraftingUIOpened', function(data) ... end)
exports['sd-crafting']:unregisterClientHook('onCraftingUIOpened', id)

All hook callbacks receive a single data table with event-specific fields. Hooks are fire-and-forget — they cannot cancel actions, and every callback is wrapped in pcall so a buggy handler from one resource cannot break others.

Timestamps on client

The timestamp field on every client hook is the value of GetGameTimer() (milliseconds since the game/resource started), not a unix timestamp. The os library is server-only in FiveM, so a real wall-clock timestamp is unavailable on the client. Use this value for ordering and relative timing of local events. Server hooks use real unix timestamps from os.time().


UI Lifecycle Events ​

onCraftingUIOpened ​

Triggered after the crafting UI has been opened and focus has been transferred to the NUI.

lua
ClientHooks.Register('onCraftingUIOpened', function(data)
    print(('Opened crafting UI at: %s (type: %s)'):format(
        data.stationId, data.workbenchType or 'unknown'))
end)
ParameterTypeDescription
stationIdstringStation identifier
workbenchTypestring|nilWorkbench type (e.g. 'basic', 'advanced')
timestampnumberUnix timestamp

onCraftingUIClosed ​

Triggered after the crafting UI has been closed and focus has been returned to the game. Fires both for clean closes and force-closes.

lua
ClientHooks.Register('onCraftingUIClosed', function(data)
    print(('Closed crafting UI at: %s'):format(data.stationId or 'unknown'))
end)
ParameterTypeDescription
stationIdstring|nilStation identifier that was being viewed
timestampnumberUnix timestamp

onForceClose ​

Triggered when the server sends a force-close (typically from the craftsimrelog admin command or a simulated reconnect). Local processing state has already been cleared at this point.

lua
ClientHooks.Register('onForceClose', function(data)
    print(('Crafting UI force-closed: %s'):format(data.reason))
end)
ParameterTypeDescription
reasonstringForce-close reason (currently 'server_force_close')
timestampnumberUnix timestamp

Local Crafting Events ​

onCraftQueued ​

Triggered when the player adds a recipe to the queue (either the local queue or the shared queue, depending on the station).

lua
ClientHooks.Register('onCraftQueued', function(data)
    print(('Queued %dx recipe %s (queue item %s)'):format(
        data.quantity, data.recipeId, data.queueItemId))
end)
ParameterTypeDescription
stationIdstringStation identifier
recipeIdstringRecipe identifier
quantitynumberQuantity queued
queueItemIdstringGenerated queue item ID
craftTimenumberTotal craft duration (recipe.craftTime * quantity)
timestampnumberUnix timestamp

onCraftCancelled ​

Triggered when the player cancels a queued (or actively-crafting) item before it completes. Refund has already been requested from the server at this point.

lua
ClientHooks.Register('onCraftCancelled', function(data)
    print(('Cancelled craft of recipe %s'):format(data.recipeId))
end)
ParameterTypeDescription
stationIdstringStation identifier
recipeIdstringRecipe identifier
queueItemIdstringQueue item ID that was cancelled
timestampnumberUnix timestamp

onCraftCompleted ​

Triggered locally after a craft finishes successfully (including partial successes) and the server has granted the output items. This is separate from the server-side onCraftCompleted — register on this one for sound/screen effects that should fire on the crafter's screen.

lua
ClientHooks.Register('onCraftCompleted', function(data)
    PlaySoundFrontend(-1, 'PURCHASE', 'HUD_LIQUOR_STORE_SOUNDSET', true)
    print(('Crafted %dx %s (%d/%d successful)'):format(
        data.quantity, data.outputItem,
        data.successfulCrafts, data.quantity))
end)
ParameterTypeDescription
stationIdstringStation identifier
recipeIdstringRecipe identifier
quantitynumberQuantity originally queued
successfulCraftsnumberNumber of items that passed the quality roll
failedCraftsnumberNumber of items that failed (partial-failure mode only)
outputItemstringItem name granted
levelDatatable|nilLevel progression data if XP was awarded (xp, level, leveledUp, etc.)
techPointsDatatable|nilTech points data if tech points were awarded
blueprintDestroyedbooleanWhether the blueprint was destroyed by this craft
timestampnumberUnix timestamp

onCraftFailed ​

Triggered locally when a craft fails the quality check entirely (no items granted, ingredients consumed).

lua
ClientHooks.Register('onCraftFailed', function(data)
    PlaySoundFrontend(-1, 'ERROR', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true)
    print(('Craft failed: %s'):format(data.recipeId))
end)
ParameterTypeDescription
stationIdstringStation identifier
recipeIdstringRecipe identifier
quantitynumberQuantity attempted
reasonstringFailure reason (currently 'quality_check')
timestampnumberUnix timestamp

Staging Inventory Events ​

onItemStaged ​

Triggered after the player successfully stages an item from inventory into the crafting staging area via the UI.

lua
ClientHooks.Register('onItemStaged', function(data)
    print(('Staged %dx %s at %s'):format(data.count, data.item, data.stationId))
end)
ParameterTypeDescription
stationIdstringStation identifier
itemstringItem name
countnumberQuantity staged
slotnumber|nilTarget staging slot
timestampnumberUnix timestamp

onItemUnstaged ​

Triggered after the player successfully unstages an item from the crafting staging area back into inventory.

lua
ClientHooks.Register('onItemUnstaged', function(data)
    print(('Unstaged %dx %s from %s'):format(data.count, data.item, data.stationId))
end)
ParameterTypeDescription
stationIdstringStation identifier
itemstringItem name
countnumberQuantity unstaged
slotnumber|nilSource staging slot
timestampnumberUnix timestamp

Blueprint Events ​

onBlueprintAttached ​

Triggered locally after a blueprint has been successfully attached to the current station.

lua
ClientHooks.Register('onBlueprintAttached', function(data)
    print(('Attached blueprint: %s'):format(data.blueprintItem))
end)
ParameterTypeDescription
stationIdstringStation identifier
blueprintItemstringBlueprint item name
timestampnumberUnix timestamp

onBlueprintDetached ​

Triggered locally after a blueprint has been successfully detached from the current station and returned to inventory.

lua
ClientHooks.Register('onBlueprintDetached', function(data)
    print(('Detached blueprint: %s'):format(data.blueprintItem))
end)
ParameterTypeDescription
stationIdstringStation identifier
blueprintItemstringBlueprint item name
timestampnumberUnix timestamp

Tech Tree Events ​

onTechNodeUnlockRequested ​

Triggered immediately before the server is asked to unlock a tech node. Useful for optimistic UI effects.

lua
ClientHooks.Register('onTechNodeUnlockRequested', function(data)
    print(('Requesting unlock: %s in tree %s'):format(data.nodeId, data.treeId))
end)
ParameterTypeDescription
treeIdstringTech tree identifier
nodeIdstringTech node identifier
workbenchTypestring|nilWorkbench type
stationIdstringStation identifier
timestampnumberUnix timestamp

onTechNodeUnlocked ​

Triggered locally after the server confirms a successful tech node unlock.

lua
ClientHooks.Register('onTechNodeUnlocked', function(data)
    PlaySoundFrontend(-1, 'CHALLENGE_UNLOCKED', 'HUD_AWARDS', true)
    print(('Unlocked %s (%d points remaining)'):format(data.nodeId, data.newPoints))
end)
ParameterTypeDescription
treeIdstringTech tree identifier
nodeIdstringTech node identifier
workbenchTypestring|nilWorkbench type
stationIdstringStation identifier
newPointsnumberTech points the player has left
isSharedbooleanWhether the unlock was applied to a shared-workbench pool
timestampnumberUnix timestamp

Workbench Placement Events ​

onWorkbenchPlaced ​

Triggered on every client when a placed workbench is spawned in the world — this is broadcast to all clients, not just the placer. Useful for ambient effects, blip-spawn animations, or audit logs.

lua
ClientHooks.Register('onWorkbenchPlaced', function(data)
    print(('Workbench %d placed at %.2f, %.2f, %.2f'):format(
        data.workbenchId, data.coords.x, data.coords.y, data.coords.z))
end)
ParameterTypeDescription
workbenchIdnumberDatabase ID of the workbench
itemstringWorkbench item name
workbenchTypestringWorkbench type
coordsvector3Placement coordinates
headingnumberPlacement heading
timestampnumberUnix timestamp

onWorkbenchPickedUp ​

Triggered locally on the owner's client after they successfully pick up their placed workbench.

lua
ClientHooks.Register('onWorkbenchPickedUp', function(data)
    print(('Picked up workbench %d'):format(data.workbenchId))
end)
ParameterTypeDescription
workbenchIdnumberDatabase ID of the workbench that was picked up
timestampnumberUnix timestamp

Notifications & Sync Events ​

onNotificationReceived ​

Triggered when the server pushes a notification to this player via sd-crafting:client:notify. Useful for routing crafting notifications through a custom notification system.

lua
ClientHooks.Register('onNotificationReceived', function(data)
    print(('[%s] %s: %s'):format(data.type, data.title, data.description))
end)
ParameterTypeDescription
titlestring|nilNotification title
descriptionstring|nilNotification body
typestring|nilNotification type ('success', 'error', 'inform', etc.)
timestampnumberUnix timestamp

onSharedQueueSynced ​

Triggered when the server pushes a shared queue update for the station the player is currently viewing.

lua
ClientHooks.Register('onSharedQueueSynced', function(data)
    print(('Shared queue at %s now has %d items'):format(
        data.stationId, data.queueLength))
end)
ParameterTypeDescription
stationIdstringStation identifier
queuetableUpdated queue array (with interpolated remainingTime)
queueLengthnumberQueue length
timestampnumberUnix timestamp

onStagedItemsSynced ​

Triggered when the server pushes a staged-items update for the station the player is currently viewing (typically because another player modified the shared staging inventory).

lua
ClientHooks.Register('onStagedItemsSynced', function(data)
    print(('Staged items at %s updated: %d items, %dg'):format(
        data.stationId, #data.items, data.totalWeight))
end)
ParameterTypeDescription
stationIdstringStation identifier
itemstableUpdated staged items array
totalWeightnumberTotal weight of staged items
timestampnumberUnix timestamp

onTechTreeSynced ​

Triggered when the server pushes a tech tree update — typically because another player on a shared workbench unlocked a node.

lua
ClientHooks.Register('onTechTreeSynced', function(data)
    print(('Tech tree synced: %d points'):format(data.techPoints))
end)
ParameterTypeDescription
techPointsnumberNew tech points value
unlockedNodestableMap of unlocked nodes
workbenchTypestring|nilWorkbench type the sync applies to
timestampnumberUnix timestamp

onAdminRecipeSynced ​

Triggered when the server pushes a recipe change (admin-driven create/update, delete, or reorder) to the client recipe cache. The action field distinguishes the three cases.

lua
ClientHooks.Register('onAdminRecipeSynced', function(data)
    if data.action == 'upsert' then
        print(('Recipe synced in %s: %s'):format(data.tableName, data.recipe.id))
    elseif data.action == 'remove' then
        print(('Recipe removed from %s: %s'):format(data.tableName, data.recipe.id))
    elseif data.action == 'reorder' then
        print(('Recipes reordered in %s'):format(data.tableName))
    end
end)
ParameterTypeDescription
tableNamestringRecipe table the change applies to
recipetableRecipe object (full recipe for upsert, { id } for remove, { orderedIds } for reorder)
actionstring'upsert', 'remove', or 'reorder'
timestampnumberUnix timestamp

Integration Examples ​

Custom Sound Design ​

lua
ClientHooks.Register('onCraftingUIOpened', function()
    PlaySoundFrontend(-1, 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true)
end)

ClientHooks.Register('onCraftCompleted', function()
    PlaySoundFrontend(-1, 'PURCHASE', 'HUD_LIQUOR_STORE_SOUNDSET', true)
end)

ClientHooks.Register('onCraftFailed', function()
    PlaySoundFrontend(-1, 'ERROR', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true)
end)

ClientHooks.Register('onTechNodeUnlocked', function()
    PlaySoundFrontend(-1, 'CHALLENGE_UNLOCKED', 'HUD_AWARDS', true)
end)

Particle FX on Workbench Placement ​

lua
ClientHooks.Register('onWorkbenchPlaced', function(data)
    RequestNamedPtfxAsset('core')
    while not HasNamedPtfxAssetLoaded('core') do Wait(0) end
    UseParticleFxAssetNextCall('core')
    StartParticleFxNonLoopedAtCoord(
        'ent_dst_dust',
        data.coords.x, data.coords.y, data.coords.z,
        0.0, 0.0, 0.0,
        1.0, false, false, false
    )
end)

Route Notifications to a Custom System ​

lua
ClientHooks.Register('onNotificationReceived', function(data)
    exports['your-notify']:Show(data.title, data.description, data.type or 'info')
end)

Integration Ideas

Use client hooks to:

  • Play custom sounds on craft completion, failure, or tech tree unlock
  • Trigger camera effects when opening or closing the crafting UI
  • Show custom notifications using your preferred notification system
  • Spawn particle FX or props when workbenches are placed
  • Apply screen shake or vignette effects on craft failure
  • Log client-side events for debugging or analytics
  • Animate the player ped on craft completion