> ## Documentation Index
> Fetch the complete documentation index at: https://docs.redutzu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Hooks

> Hooks that let you intercept and modify MDT actions.

Hooks run on the **server**, **before** an action happens. A hook can let the action through, block it, or change its data. [Events](https://docs.redutzu.com/redutzu-mdt/api-reference/events.md) are different: they fire **after** an action and only tell you that it happened.

## Registering a hook

```lua theme={null}
local hookId = exports['redutzu-mdt']:RegisterHook('onIncidentCreate', function(payload)
    print(('%s is creating incident "%s"'):format(payload.source, payload.data.title))
end, { priority = 10 })
```

| Parameter | Type | Description |
| - | - | - |
| `name` | string | Hook name, for example `onChatSend` |
| `fn` | function | Handler, receives the payload table |
| `options` | table? | `{ print?, once?, priority? }` |

| Option | Type | Description |
| - | - | - |
| `priority` | number | Higher runs first, equal priority keeps registration order. Default `0` |
| `once` | boolean | Remove the hook after it runs once |
| `print` | boolean | Print to the console every time the hook runs |

`RegisterHook` returns a hook id. Pass it to `RemoveHook` to unregister:

```lua theme={null}
exports['redutzu-mdt']:RemoveHook(hookId)
```

Hooks are removed automatically when the resource that registered them stops.

## Return values

| Return | Result |
| - | - |
| `nil` / `true` | The action goes through unchanged |
| `false, reason?` | The action is blocked. `reason` is sent back to the UI as the error |
| `table` | The action goes through with the changed fields |

> [!NOTE]
>
> Only the fields listed under **Changeable** are read back from a returned table. Any other field you change is ignored, and the MDT validates the new values again, the same way it validates player input.

* `payload.source` is the player who triggered the action. It's `nil` when the action is automated (exports, timers, scripts).
* If a hook errors, the error is logged and the hook is skipped. The action still goes through.
* The first hook to return `false` stops the chain. Hooks after it don't run.

## Examples

### Block an action

Stop society withdrawals over a limit:

```lua theme={null}
exports['redutzu-mdt']:RegisterHook('onAdjustSocietyFunds', function(payload)
    if payload.type == 'withdraw' and payload.amount > 50000 then
        return false, 'Withdrawals over $50,000 need approval'
    end
end)
```

### Change the payload

Return the payload with the changed fields. Every hook after this one receives the changed payload.

```lua theme={null}
local blocked = { 'badword', 'anotherword' }

exports['redutzu-mdt']:RegisterHook('onChatSend', function(payload)
    local content = payload.content

    for _, word in ipairs(blocked) do
        content = content:gsub(word, ('*'):rep(#word))
    end

    payload.content = content
    return payload
end)
```

### Adjust charges

`onChargesIssue` runs before any fine, jail time or record is written, for every flow that issues charges (penalties, incidents and warrants). Returned charges must still target the same citizens and stay within the `Config.Charges` caps, otherwise the action is rejected.

```lua theme={null}
exports['redutzu-mdt']:RegisterHook('onChargesIssue', function(payload)
    for _, charge in ipairs(payload.charges) do
        -- charge = { identifier, qty, fine, jail, community_service, bail, points, ... }
        charge.fine = math.floor(charge.fine * 0.5)
    end

    return payload
end)
```

### Enrich dispatch calls

`payload.payload` holds the call being created (`type`, `code`, `priority`, `title`, `description`, `location`, `details`).

```lua theme={null}
exports['redutzu-mdt']:RegisterHook('onDispatchCallCreate', function(payload)
    local call = payload.payload

    if call.type == 'speeding' then
        call.priority = 'medium'
    end

    return payload
end)
```

### Require something before clock in

Any check your server already has can gate an MDT action. `my_garage` below stands in for your own resource.

```lua theme={null}
exports['redutzu-mdt']:RegisterHook('onShiftClockIn', function(payload)
    if not exports.my_garage:HasPatrolCar(payload.identifier) then
        return false, 'Grab a patrol car first'
    end
end)
```

## Hook list

Most payloads also include `source`. It isn't repeated in the tables below.

### Incidents

| Name | Payload | Changeable |
| - | - | - |
| `onIncidentCreate` | `data` | `data` |
| `onIncidentUpdate` | `id`, `data` | `data` |
| `onIncidentDelete` | `id` | |
| `onChargesIssue` | `entityType`, `entityId`, `charges` | `charges` |

`entityType` is `penalty`, `incident` or `warrant`. `entityId` is `nil` while the record is still being created.

### Warrants

| Name | Payload | Changeable |
| - | - | - |
| `onWarrantCreate` | `data` | `data` |
| `onWarrantUpdate` | `id`, `status` | `status` |
| `onWarrantServe` | `id`, `citizen`, `charges` | |
| `onWarrantDelete` | `id` | |
| `onOperationPlanCreate` | `data` | `data` |
| `onOperationPlanUpdate` | `id`, `data` | `data` |
| `onOperationPlanMission` | `id`, `action` | |
| `onOperationPlanDelete` | `id` | |

### BOLOs

| Name | Payload | Changeable |
| - | - | - |
| `onBoloCreate` | `data` | `data` |
| `onBoloUpdate` | `id`, `status` | `status` |
| `onBoloLocate` | `id` | |
| `onBoloDelete` | `id` | |

### Evidence

| Name | Payload | Changeable |
| - | - | - |
| `onEvidenceCreate` | `data` | `data` |
| `onEvidenceUpdate` | `id`, `data` | `data` |
| `onEvidenceDelete` | `id` | |
| `onEvidenceCustodyTransfer` | `id`, `data` | `data` |
| `onEvidenceAnalysisRequest` | `id`, `data` | `data` |
| `onEvidenceAnalysisComplete` | `id`, `evidenceId`, `data` | `data` |

### Citizens

| Name | Payload | Changeable |
| - | - | - |
| `onCitizenProfileUpdate` | `identifier`, `data` | `data` |
| `onCitizenImageChange` | `identifier`, `url` | `url` |
| `onCitizenLicenseChange` | `identifier`, `licenseType`, `active` | |
| `onCitizenLicensePointsChange` | `identifier`, `points` | |
| `onLicensePointsThreshold` | `identifier`, `points`, `total`, `threshold` | |

### Vehicles & weapons

| Name | Payload | Changeable |
| - | - | - |
| `onVehicleUpdate` | `plate`, `patch` | `patch` |
| `onVehicleProfileUpdate` | `plate`, `data` | `data` |
| `onVehicleImageChange` | `plate`, `url` | `url` |
| `onVehicleModificationAdd` | `plate`, `data` | `data` |
| `onVehicleModificationRemove` | `plate`, `modification` | |
| `onWeaponCreate` | `data` | `data` |
| `onWeaponUpdate` | `serial`, `status` | `status` |

`modification` is `{ id, name, category }`.

### Dispatch

| Name | Payload | Changeable |
| - | - | - |
| `onDispatchCallCreate` | `payload`, `coords` | `payload`, `coords` |
| `onDispatchCallAttach` | `id`, `callsign` | |
| `onDispatchCallDetach` | `id`, `callsign` | |
| `onDispatchCallAcknowledge` | `id`, `callsign` | |
| `onDispatchCallNote` | `id`, `text`, `author` | `text` |
| `onDispatchCallPriorityBump` | `id`, `from`, `to` | `to` |
| `onDispatchCallResolve` | `id`, `call`, `cause` | |

### Field

| Name | Payload | Changeable |
| - | - | - |
| `onRadarPlateHit` | `alert` | `alert` |
| `onSuspectCuff` | `target`, `cuffed` | |

Blocking `onRadarPlateHit` hides the hit from the officer and skips the dispatch call it would create.

### Chat

| Name | Payload | Changeable |
| - | - | - |
| `onChatSend` | `channel`, `content`, `replyTo`, `forwardedFrom` | `channel`, `content`, `replyTo`, `forwardedFrom` |
| `onChatSendImage` | `channel`, `image`, `content`, `forwardedFrom` | `channel`, `image`, `content`, `forwardedFrom` |
| `onChatSendLocation` | `channel`, `location`, `live`, `forwardedFrom` | `channel`, `location`, `live`, `forwardedFrom` |
| `onChatUpdateLocation` | `messageId`, `channel`, `location`, `live` | `location`, `live` |
| `onChatShareRecord` | `channel`, `record`, `forwardedFrom` | `channel`, `record`, `forwardedFrom` |
| `onChatEdit` | `messageId`, `channel`, `content` | `content` |
| `onChatDelete` | `messageId`, `channel` | |
| `onChatReact` | `messageId`, `channel`, `emoji` | `emoji` |
| `onChatPin` | `messageId`, `channel` | |
| `onChatUnpin` | `messageId`, `channel` | |
| `onChatCreatePoll` | `channel`, `question`, `optionLabels` | `channel`, `question`, `optionLabels` |
| `onChatVote` | `messageId`, `channel`, `optionId` | `optionId` |
| `onChatOpenDm` | `targetIdentifier` | `targetIdentifier` |
| `onChatPostSystemMessage` | `channel`, `key`, `data`, `content` | `channel`, `key`, `data`, `content` |

`image` is `{ url, width?, height? }`, `location` is `{ label?, x, y }` and `record` is `{ kind, id, label }`.

### Notifications

| Name | Payload | Changeable |
| - | - | - |
| `onNotificationSend` | `type`, `entries` | `entries` |

### Collaboration

| Name | Payload | Changeable |
| - | - | - |
| `onCollabRoomCreate` | `visibility`, `maxPlayers` | `visibility`, `maxPlayers` |
| `onCollabRoomJoin` | `docId` | |
| `onCollabRoomClose` | `docId` | |
| `onCollabMemberInvite` | `docId`, `targetSource` | |
| `onCollabMemberKick` | `docId`, `targetIdentifier`, `targetSource`, `ban` | `ban` |
| `onCollabOwnershipTransfer` | `docId`, `ownerId`, `targetIdentifier`, `targetSource` | |

### Employees

| Name | Payload | Changeable |
| - | - | - |
| `onEmployeeRecruit` | `identifier`, `job`, `grade`, `callsign` | `grade`, `callsign` |
| `onEmployeeFire` | `identifier`, `job`, `reason` | |
| `onEmployeeGradeChange` | `identifier`, `job`, `fromGrade`, `toGrade` | `toGrade` |
| `onEmployeeCallsignChange` | `identifier`, `job`, `callsign` | `callsign` |
| `onEmployeeBadgeChange` | `identifier`, `job`, `badge` | `badge` |
| `onEmployeeStrikeAdd` | `identifier`, `job`, `reason` | `reason` |
| `onEmployeeReviewCreate` | `identifier`, `job`, `score`, `ratings`, `notes` | |
| `onEmployeeReviewUpdate` | `id`, `score`, `ratings`, `notes` | |
| `onEmployeeHRNoteCreate` | `identifier`, `job`, `type`, `severity`, `body` | |
| `onEmployeeHRNoteUpdate` | `id`, `type`, `severity`, `status`, `body` | |

### Shifts

| Name | Payload | Changeable |
| - | - | - |
| `onShiftClockIn` | `identifier`, `job` | `job` |
| `onShiftClockOut` | `identifier`, `reason` | `reason` |

### Certifications & achievements

| Name | Payload | Changeable |
| - | - | - |
| `onCertificationGrant` | `identifier`, `job`, `certificationTypeId` | |
| `onCertificationRevoke` | `identifier`, `job`, `certificationTypeId` | |
| `onCertificationTypeCreate` | `job`, `data` | `data` |
| `onCertificationTypeUpdate` | `id`, `job`, `data` | `data` |
| `onCertificationTypeDelete` | `id`, `job` | |
| `onAchievementCreate` | `job`, `data` | `data` |
| `onAchievementUpdate` | `id`, `job`, `data` | `data` |
| `onAchievementDelete` | `id`, `job` | |

### Department

| Name | Payload | Changeable |
| - | - | - |
| `onAdjustSocietyFunds` | `job`, `type`, `amount`, `reason` | `amount`, `reason` |
| `onRecordAccessChange` | `entityType`, `id`, `visibility`, `departments` | `visibility`, `departments` |
| `onRadioCodeCreate` | `data` | `data` |
| `onRadioCodeUpdate` | `id`, `data` | `data` |
| `onRadioCodeDelete` | `id` | |
| `onTagCreate` | `data` | `data` |
| `onTagUpdate` | `id`, `data` | `data` |
| `onTagDelete` | `id` | |

For `onRecordAccessChange`, `entityType` is `incident`, `warrant`, `bolo` or `evidence`.

### Penal code & settings

| Name | Payload | Changeable |
| - | - | - |
| `onPenalCategoryUpdate` | `id`, `data` | `data` |
| `onPenalCategoryDelete` | `id` | |
| `onPenalArticleUpdate` | `id`, `data` | `data` |
| `onPenalArticleDelete` | `id` | |
| `onAdminSettingChange` | `key`, `value` | `value` |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.