Lifecycle Hooks
Lifecycle hooks let you run scripts (.bat, .cmd, .ps1, or .py) automatically at specific points in a DayZ server's lifecycle. They're useful for pre-start health checks, post-stop cleanup, crash alerting, or hooking into your own infrastructure (Slack pages, Datadog events, S3 backups, etc.) without modifying Citadel itself.
Where they live
For each server, drop scripts under:
<installDir>/lifecycle_hooks/
<installDir> is the path you configured for the DayZ server in the dashboard — the same directory that holds DayZServer_x64.exe. The lifecycle_hooks/ directory is created automatically the first time you save a script there from the file browser.
Events
Citadel calls scripts whose filename matches the event name. All four events are optional — only the events you actually have a script for fire.
| Event | Filename | Behaviour |
|---|---|---|
| pre-start | pre-start.{bat,cmd,ps1,py} | Runs before the DayZ server process starts. Blocking — Citadel waits for the script to exit. A non-zero exit code aborts the start and the failure is surfaced in the dashboard. Use for health checks, mounting drives, regenerating configs. |
| started | started.{bat,cmd,ps1,py} | Runs after the server is up and accepting connections. Fire-and-forget — Citadel doesn't wait. Use for "server X is up" notifications, updating external status boards. |
| stopped | stopped.{bat,cmd,ps1,py} | Runs after a graceful stop completes. Fire-and-forget. Use for log rotation, backup uploads, "going offline" notifications. |
| crashed | crashed.{bat,cmd,ps1,py} | Runs when Citadel detects the DayZ process exited unexpectedly (non-zero exit code that wasn't a graceful stop). Fire-and-forget. Use for paging, crash-dump uploads, auto-restart-throttling logic. |
If two scripts match the same event with different extensions, the first one Citadel finds (in the order above) wins. Don't rely on dual-script behavior.
Environment variables
Hook scripts get the following environment variables injected so they can act on the right server without hard-coding:
| Variable | Example | Description |
|---|---|---|
CITADEL_SERVER_ID | srv_4f3a | Citadel's internal server id |
CITADEL_SERVER_NAME | Chernarus PvE | Display name from the dashboard |
CITADEL_SERVER_INSTALL_DIR | C:\DayZServer | The server's install directory |
CITADEL_SERVER_PORT | 2302 | Game port |
CITADEL_SERVER_RCON_PORT | 2303 | RCON port |
CITADEL_EVENT | pre-start | The event that triggered this script |
CITADEL_EXIT_CODE | 139 | (crashed event only) The non-zero exit code from DayZ |
Scripts are launched with the lifecycle_hooks/ directory as the working directory.
Editing scripts from the dashboard
Writing scripts via the file browser requires the files.edit-scripts permission on your Citadel role. This is intentional: anyone who can save a .bat or .ps1 to disk can effectively run code on the server box, so the permission is a separate gate from the general files.edit permission used for editing config files. None of the built-in roles include files.edit-scripts by default — grant it explicitly to roles that need it under Settings → Users & Roles.
The path constraint matters too:
Script writes (
.bat/.cmd/.ps1/.sh) only succeed when the resolved destination path is inside<installDir>/lifecycle_hooks/. Writes outside that directory return403withfile.write-blockedin the audit log.
This means you can't use the file browser to drop a .ps1 into C:\Windows\System32\ — the editor refuses, even with files.edit-scripts. If you need to script outside lifecycle_hooks/, do it via the OS shell on the server, not via Citadel.
Examples
pre-start health check (PowerShell)
# pre-start.ps1 — refuse to start if the data drive is < 5 GB free
$drive = Get-PSDrive C
$freeGB = [math]::Round($drive.Free / 1GB, 1)
if ($freeGB -lt 5) {
Write-Error "Refusing to start ${env:CITADEL_SERVER_NAME}: only $freeGB GB free on C:"
exit 1
}
exit 0
crashed → Slack alert (PowerShell)
# crashed.ps1 — POST to a Slack incoming webhook
$payload = @{
text = ":rotating_light: ${env:CITADEL_SERVER_NAME} crashed (exit $env:CITADEL_EXIT_CODE)"
} | ConvertTo-Json
Invoke-RestMethod -Uri 'https://hooks.slack.com/services/...' -Method Post -Body $payload -ContentType 'application/json'
started → log rotation (Batch)
:: started.bat — archive yesterday's RPT before the new one fills up
forfiles /p "%CITADEL_SERVER_INSTALL_DIR%\profiles" /m *.RPT /d -1 /c "cmd /c move @file archive\@file"
Audit trail
Every hook execution is recorded in the Citadel audit log with the event name, server id, exit code, and duration. See lifecycle.hook.start / lifecycle.hook.complete / lifecycle.hook.error in the Audit Log Codes reference.
Why a separate permission
Allowing arbitrary script edits via the same permission used for editing serverDZ.cfg would mean any user with config-edit access could escalate to remote code execution on the box. Splitting files.edit-scripts out lets operators give "config editor" access to people they trust with config but not with shell — which is the common case.