Skip to main content

Actions

Actions are code snippets executed when a log event is imported from the game server. Use them to process data and automate responses.

Actions now support both a server-local workflow and a marketplace workflow. For the shared model behind marketplace, installed, and custom actions, see Action marketplace concept.

Data source note: actions depend on imported server logs. If logs arrive later, action execution is also delayed. For full import/freshness behavior, see Logs import concept.

  • Server Admin -> Actions

Tabsโ€‹

The actions screen is split into three tabs:

  • Marketplace: browse public creator actions and install them on the current server
  • Installed: manage marketplace actions already installed on this server
  • Custom Actions: create and maintain server-owned actions with local Lua code

Installed marketplace actions inherit their code, event trigger, and config schema from the creator definition. Server staff only manage the active state and configuration values for that server.

Ready-made reward actionsโ€‹

  • Kill-Reward pays the killer for every PvP kill and can take a penalty from the victim. Optional settings limit which kills pay: no reward for spawn kills, no reward for killing a player who had been alive for fewer than a set number of minutes (a longer protection for fresh spawns), no reward below a minimum K/D of the victim, and a tier table that pays by the victim's K/D โ€” either a fixed amount per tier or a factor on the kill reward. Each tier applies from its K/D up to the next tier's. The K/D is the one dzbot shows on the victim's profile. Left empty, every kill pays the kill reward as before.
  • Group Wage pays every member of a dzbot group (Admin โ†’ Groups) a fixed amount each day, week (starting Monday) or month. It runs from the scheduler: after installing it, add a scheduler task Run action for it with the times you want, e.g. every Monday at 18:00. Each member is paid at most once per period in your server's time zone โ€” running the task again, by hand or from a second task, pays nobody twice. It can pay only members who were on the server within the last few days, send each paid member a Discord DM and post a summary to a channel. The transaction text is yours to set. Install it on one server only: the wallet is shared across the organization, so a copy on a second server pays the same members again.

Ready-made spawn-kill actionsโ€‹

Both go by the spawn kill label by default, so they follow the setting PvP kill: minimum victim survival time (2 minutes unless you changed it) โ€” the same kills the kill feed marks and the statistics leave out. Each also offers its own limit in minutes instead, counted in 5-minute steps.

  • Spawn-Trap Detection posts every spawn kill to a moderation channel and counts it in the killer's fresh_spawn_kills variable, shown on the player detail page. Kills by players who had themselves been alive for fewer than a set number of minutes (default 30) are left out, so two fresh spawns fighting each other do not count.
  • Fresh Spawn Autoban bans a player who makes a set number of spawn kills within a rolling window (default 5 in 10 minutes) and can post each ban to a channel.
  • Raid Autoban bans a player who destroys a base structure at once โ€” a chainsaw through a fence or a gate โ€” outside the raid windows you list (e.g. Saturday & Sunday 18:00 โ€“ 23:59; a window may cross midnight). With no windows, every such raid is banned. Ban length, scope and reason are configurable, a filter limits it to zones, factions or groups, and report-only mode posts raids to a channel without banning. Dismantling with tools is not covered: the log cannot tell an owner taking down their own wall from a raider. It also bans the accomplices: everyone who was within 100 m of the raid in the 10 minutes before it and does not belong to the raided base. The base side is everyone with at least 3 logged build actions at that base, their other accounts, and every member of their factions and of those factions' allies. That way one player cannot sacrifice themself while the rest of the group raids on. Positions are only logged every few minutes, so somebody who just walked past can be caught too โ€” the report lists everyone who was banned along with the raider, and you lift the wrong bans by hand. Radius, minutes and the minimum build actions are configurable; switch accomplices off to ban only the raider. No accomplice is banned when no known base stands at the spot.
  • Explosive Raid Autoban does the same when somebody places explosives inside a base they do not belong to, outside the raid windows. DayZ does not log a wall blown up by explosives, only the placing of the charge, so this is the only way to catch an explosive raid. By default plastic and improvised explosives count, claymores do not (they are placed for defence too); the list of class names is configurable. Explosives placed outside any known base or in the placer's own base are ignored. Accomplices are banned as with Raid Autoban.

Ready-made Discord feedsโ€‹

Each posts one kind of event to a Discord channel you pick. Every feed has an event filter (which events count โ€” see Event filter fields) and a Discord post you design with a live preview (see Discord embed fields). They run next to the built-in kill and damage feed from Settings โ†’ Discord, which stays as it is.

  • Login Feed and Logout Feed โ€” a player joined or left. The position is off by default; switch the Location field on only in an admin channel.
  • Build Feed and Placement Feed โ€” every base part built and every tent, barrel, flag or trap placed, with a map link. Meant for an admin channel: it shows where bases are.
  • PvE Death Feed โ€” one action per kind of death, install the ones you want: Infected, Animals & Environment (wolves, bears, mines, traps, explosives, vehicles, fire, falls โ€” the cause as DayZ logs it), Bled Out, Drowned and Suicide (loud on busy servers: many players kill themselves to respawn).
  • Long-Range Kill Feed โ€” player kills from at least 300 m; change the distance in the filter.
  • Kill Streak Feed โ€” posts when a player reaches a kill streak milestone (3, 5, 10 โ€ฆ by default), when someone ends a streak of at least 5, and, if you switch it on, death streaks. Streaks live in the player variables feed_kill_streak and feed_death_streak, count from the moment you install the action and end only with a death to another player. The filter decides which kills count.
  • Rich Kill Feed โ€” every player kill with its kill flags under the title, weapon and distance. Use it for a second channel, e.g. headshots only.
  • Admin Kill Feed โ€” every player kill with weapon, distance, flags, a map link, the zones, the killer's position and both player IDs, for an admin-only channel.
  • Raid Feed โ€” every base structure destroyed at once (a chainsaw through a fence or a gate), with the tool and a map link.

An action has one fixed trigger, which is why logins and logouts, building and placing, and each kind of PvE death are separate actions. Each run is listed on the action's Logs tab, including the reason when the filter skipped an event.

Access requirementsโ€‹

Actions now have three permission levels:

  • actions.edit: Allows editing server-side configuration values and enabling or disabling installed or custom actions
  • actions.manage_marketplace: Allows installing marketplace actions and removing installed marketplace actions
  • actions.edit_code: Allows creating and maintaining custom actions, including the Lua code and configuration schema

This separation allows server administrators to distinguish between operational action settings, marketplace management, and local code authoring.

Action language: Luaโ€‹

The actions are written in Lua, a lightweight, high-level, multi-paradigm programming language designed primarily for embedded use in applications.

Action scripts run directly. Two globals are available:

  • data: Event payload of the current log event
  • config: Resolved action configuration values

To preview values while building scripts, use the print function.

Here is an example of a simple action that prints the log event data to the console:

print(data);

Kill context in data.flagsโ€‹

player_kill and player_killed carry the kill context in data.flags: headshot, revenge, trade, team_kill, alliance_violation and spawn_kill are true or false; multi_kill is the number of kills the killer made within 45 seconds (2, 3, 4 โ€ฆ) and nil for a single kill. player_hit_player and player_hit_by_player carry data.flags.friendly_fire.

player_kill also tells how long the victim had been alive: data.victim.minutes_alive in 5-minute steps, and data.victim.seconds_alive, exact to the second during the first 5 minutes of a life; nil when dzbot cannot tell when the victim's life started.

if data.flags.headshot then
player.change_balance(data.killer.username, 50, "Headshot bonus");
end

if data.flags.multi_kill and data.flags.multi_kill >= 3 then
discord.send_to_channel(config.channelId, data.killer.username .. " got " .. data.flags.multi_kill .. " kills in 45 seconds!");
end

Zones of an eventโ€‹

Every event with a position (kills, hits, deaths, building, placing, flags, the five-minute player_online ping and more) carries the zones it happened in as data.zones โ€” a list with the id and name (in the server language) of each zone. It is an empty list when the event happened in no zone. Inactive zones are not listed; a zone you change counts for new events within about ten seconds.

for _, zone in ipairs(data.zones) do
print("Happened in " .. zone.name .. " (#" .. zone.id .. ")");
end

Let the operator pick the zones with a zone_select field and compare its value with tostring(zone.id):

local wanted = {};
for _, id in ipairs(config.zones or {}) do wanted[tostring(id)] = true end
for _, zone in ipairs(data.zones) do
if wanted[tostring(zone.id)] then
player.change_balance(data.killer.username, 100, "Kill in " .. zone.name);
break;
end
end

The Log Explorer stores the zones with the event too, so you can see where an event was afterwards.

Configuration fieldsโ€‹

Actions support user-defined configuration fields that allow you to parameterize your Lua scripts without editing the code. This is useful for:

  • Allowing moderators to adjust action behavior without code access
  • Reusing the same action template with different settings
  • Keeping sensitive values (like channel IDs) separate from the code

For marketplace actions, these fields are defined by the creator. Server admins only set the values in the installed server copy.

Defining configuration fieldsโ€‹

For custom actions, configuration fields are defined in the Code tab using the configuration schema builder.

For marketplace actions, configuration fields are defined in Creator Space by the action author and then appear on each server installation.

Each field has:

  • Name: Key used in Lua (config.<name>)
  • Type: see Field types below
  • Default: Fallback value used when no value is saved in Settings
  • Description: Help text shown in the Settings tab
  • Required: the action cannot be saved, and does not run, while the field is visible and empty

Recommended naming for field names: camelCase or snake_case without spaces.

Sections and conditionsโ€‹

Add a section to group the fields after it under a heading, up to the next section. A section holds no value and never appears in config.

Every field and every section can carry a condition under Only show when โ€ฆ: pick a field above it, an operator (is, is not, is one of, is set, is greater than, contains โ€ฆ) and a value, and combine rules with all of these apply / at least one applies, nested groups and not. A condition on a section hides every field in it.

A typical layout: a block of on/off switches at the top (ban, send a DM, open a ticket), then one section per switch that is only visible while it is on and holds that step's details. The operator then only sees the settings of what they actually use.

Rules that follow from it:

  • A condition can only read fields above it. The builder refuses a move that would break one, and renaming a field updates every condition that reads it.
  • A hidden field does not apply: it reaches the script as nil, even when it has a default, and it is never required. Its saved value is kept, so switching the controlling field back on restores it.
  • A hidden field counts as empty for the conditions of the fields after it, so hiding one field hides everything that depends on it.

Setting configuration valuesโ€‹

After a field is defined in the Code tab, users with actions.edit can set its value in the Settings tab.

At runtime, config values are resolved as:

  1. nil if the field is hidden by its condition or its section
  2. Otherwise the saved value from Settings tab (if present)
  3. Otherwise the field default from schema
  4. Otherwise nil

Only fields defined in the schema are exposed in config.

Field typesโ€‹

TypeLua valueNotes
stringstring|nilSingle-line text; empty โ†’ nil
textareastring|nilMulti-line text; identical to string in Lua
emailstring|nilEmail address input; identical to string in Lua
numbernumber|nilParsed as int or float
booleanbool|nilCheckbox; empty โ†’ nil
coordinatetable|nilMap picker โ€” access .x, .z, optionally .y, .radius
generic_selectstring|nil or string[]Dropdown from a fixed list; string[] when multiple: true
faction_selectstring|nil or string[]Server's faction IDs
player_selectstring|nil or string[]Server's player usernames
zone_selectstring|nil or string[]Server's zone IDs โ€” compare with tostring(zone.id) from data.zones
relation_selectstring[]Always an array; never nil
discord_channel_selectstring|nil or string[]Discord text channel IDs
discord_category_selectstring|nil or string[]Discord category IDs
discord_role_selectstring|nil or string[]Discord role IDs
discord_guild_selectstring|nil or string[]Discord guild IDs
object_listtable[]Repeatable list of objects; never nil โ€” empty returns {}
event_filtertableConditions picked in a form โ€” pass it to filter.matches(config.<name>, data). Actions only
discord_embedtable|nilA Discord post designed with a live preview โ€” send it with discord.send_embed(channel, config.<name>, data). Actions only

Empty values are treated as nil unless noted otherwise.

Pick the type by what the field identifies, not by the Lua value it yields: a Discord channel, role, category, or guild โ†’ the matching discord_*_select; a player โ†’ player_select; a faction โ†’ faction_select; a zone โ†’ zone_select; a fixed set of choices โ†’ generic_select. A select's value is already the ID (or username) string the action functions expect, while a plain string field leaves an error-prone free-text box. Use string only for genuinely free-form text.

Event filter fieldsโ€‹

An event_filter field lets the operator decide which events the action reacts to, in a form instead of in Lua. In the settings they add conditions one by one โ€” only those the action's event can answer are offered (a login has no distance, a build no victim):

  • Zones: the event happened inside one of the picked zones โ€” or, with Outside, in none of them
  • Distance: at least and/or at most so many metres (kills; for hits the distance between attacker and victim)
  • Weapon or item: the weapon (or, for building, placing and packing, the item) contains one of the words, ignoring case โ€” mosin matches Mosin 91/30
  • Kill flags: each flag Any, Yes (must be set) or No (must not be set) โ€” headshot, multi-kill, revenge, trade, team kill, alliance violation, friendly fire, spawn kill
  • Killer / victim (or attacker, sender, player โ€ฆ โ€” whatever the event calls them): member of one of the picked factions, of one of the picked dzbot groups, and player variables compared with is, is not, greater than, contains, is empty and the like

An event counts only when it meets every condition; no conditions means every event counts. The script checks it in one line with filter.matches. Player conditions read the database once per player and run, everything else costs nothing.

Discord embed fieldsโ€‹

A discord_embed field is a Discord post the operator designs in the settings, with a preview of how Discord will show it, filled in with sample data of the action's event: title (optionally a link), text, colour, fields that can be switched off, put side by side and dragged into order, footer, thumbnail and the time of the event. Click a placeholder to insert it where the cursor is.

Placeholders are the event's data as dot paths โ€” {killer.username}, {weapon}, {distance}, {time.string} โ€” plus {server.name}, {zone} (first zone), {zones} (all), {flags} (the kill flags as words, in the server's language) and for events with a position {location.place}, {location.label}, {location.url} (iZurvive) and {location.link} (a ready map link). Names and other texts from the event are escaped, so a player called **boss** shows literally; Discord's limits are applied (title 256 characters, text 4096, field value 1024, 25 fields, 6000 in total). The script sends it with discord.send_embed. Values the script adds to data itself (a streak count, say) can be offered as extra placeholders: list them in the field's settings as name|example value lines.

Using configuration in Luaโ€‹

Configuration values are available via the config table:

-- Access configuration values
discord.send_to_channel(config.channelId, "Player killed: " .. data.victim.username);

if config.notifyOnHeadshot and data.weapon == "Mosin" then
discord.send_to_channel(config.channelId, "Mosin kill detected");
end

if data.distance > config.minDistanceForBonus then
player.change_balance(data.killer.username, config.bonusAmount, "Long range kill bonus");
end

Example configurationโ€‹

For a kill notification action, you might define:

NameTypeDefaultDescription
channelIddiscord_channel_selectDiscord channel for notifications
notifyOnHeadshotbooleantrueSend extra message for headshots
minDistanceForBonusnumber200Minimum distance for bonus reward
bonusAmountnumber50Currency bonus for long-range kills

Moderators can then adjust these values in the Settings tab without needing access to the code.

Functionsโ€‹

To customize the action taken on a new log entry, the code editor provides searchable autocomplete using [CTRL] + [SPACE].

The following functions are available to use in the actions:

Prints the data to the console.

print({name = "John", age = 30}); -- this will be printed as json
print("Hello, World!");

Server namespaceโ€‹

The server namespace contains all server-related functions.

server.name()โ€‹

Returns the name of the server.

print(server.name);

server.current_time()โ€‹

Returns the current time as a Unix timestamp (seconds).

print(server.current_time());

server.ban_player(username: string, reason: string = "", length: string = "99 years", includeAllAccounts: bool = false, orgWide: bool = false)โ€‹

Creates a ban restriction for the player with the given username. The ban is visible and manageable in Admin โ†’ Restrictions. By default it applies to this server only and to that one account. Set orgWide to ban across every server of the organization, and includeAllAccounts to include the player's other known accounts. Pass nil for length if you only want to set the flags after it.

server.ban_player("John");
server.ban_player("John", "Cheating");
server.ban_player("John", "Cheating", "1 day");

server.currency()โ€‹

Returns the organization's in-game currency symbol as configured in Settings โ†’ Economy. The symbol is shared across all servers in the organization.

print(server.currency()); -- e.g. "Coins"
discord.send_to_channel(config.channelId, "You earned 100 " .. server.currency() .. "!");

Discord namespaceโ€‹

The discord namespace contains all Discord-related functions.

Attention: All Discord ID must be represented as strings (encapsulated in quotes), otherwise the programming language will interpret it as a Number and represents ints mathematically instead of the raw number and your action will fail.

discord.send_to_player(username: string, content: string, image?: table)โ€‹

Sends a message to the player with the given username. Pass an image from map.image as the third argument to attach it.

discord.send_to_player("John", "Hello, John!");
discord.send_to_player("h3eh23ieuh3ei2he3", "Hello, John!"); -- throws an "player not found" exception that stops the action

discord.send_to_channel(channel: string, content: string, image?: table)โ€‹

Sends a message to the channel with the given ID. Pass an image from map.image as the third argument to attach it. When the message contains an <embed> without its own <image>, the map is shown inside the embed.

discord.send_to_channel("1234567890", "Hello, World!");

local img = map.image({ points = {{ x = data.location.x, z = data.location.z, label = data.victim.username }} });
discord.send_to_channel(config.channel, data.victim.username .. " died here", img);

discord.mention(username: string)โ€‹

Returns the mention string for the player with the given username. This can be used within discord messages to reference the discord account. If no player is found, it will return the username with no formatting.

print(discord.mention("John")); -- returns "<@1234567890>"
print(discord.mention("h3eh23ieuh3ei2he3")); -- returns "h3eh23ieuh3ei"

discord.avatar(username: string)โ€‹

Returns the link to the Discord profile picture of the player with the given username, for an embed's thumbnail or author icon. Returns nil when the player is unknown, has no linked Discord account or uses Discord's default picture.

data.killer_avatar = discord.avatar(data.killer.username)
discord.send_embed(config.channel, config.embed, data) -- thumbnail of the embed field: {killer_avatar}

discord.send_embed(channel: string, embed: table, data: table)โ€‹

Fills a discord_embed config field in from data and posts it to the channel. Values the event does not carry can be set on data first. Returns true once queued; an embed field that is not set up stops the action with an error.

if not filter.matches(config.filter, data) then return end
data.streak = 5
discord.send_embed(config.channel, config.embed, data)

Filter namespaceโ€‹

filter.matches(filter: table, data: table)โ€‹

Whether the event meets the conditions of an event_filter config field (see Event filter fields). Returns true, or false and the reason as a second value. nil or an empty filter matches every event.

local ok, why = filter.matches(config.filter, data)
if not ok then
print("Skipped: " .. why) -- e.g. "distance 120 m is below 300 m"
return
end

Player namespaceโ€‹

The player namespace contains all player-related functions.

player.change_balance(username: string, amount: number, description: string = "")โ€‹

Changes the balance of the player with the given username. The balance is the player's single, org-wide wallet โ€” shared across every server in the organization, not a per-server total. The player is looked up by username on the current server.

player.change_balance("John", 100);
player.change_balance("John", 100, "Nice shot!");
player.change_balance("John", -100, "Terrible shot!"); -- negative amount to subtract

player.get_balance(username: string)โ€‹

Returns the player's current org-wide balance โ€” their single balance across the whole organization โ€” or nil if no matching player exists on the current server.

local balance = player.get_balance("John");
if balance ~= nil then
print("Balance: " .. balance);
end

player.transfer_balance(from: string, to: string, amount: number, description: string = "")โ€‹

Moves amount from one player's wallet to another's as a single transfer: either both sides are booked or neither, so a failed script can never make money disappear. Use it instead of two player.change_balance calls whenever money changes hands between players. Both players are looked up on the current server, and the ledger shows it as a transfer. It fails without moving anything if a player is not found, amount is not a positive whole number, both names belong to the same wallet, or from has too little money. Returns debit_transaction_id, credit_transaction_id, from_balance and to_balance (the new balances).

local result = player.transfer_balance("John", "Jane", 100, "Car purchase");
print("John has " .. result.from_balance .. " left");

player.statistics(username: string)โ€‹

Returns a statistics object for the player with the given username. Returns nil if no matching player exists.

local stats = player.statistics("John");
print(stats.online_streak); -- prints the current online day streak

Return fields:

FieldTypeDescription
pvp_killsintTotal PvP kills
last_pvp_killstring|nilTimestamp of last kill
pvp_deathsintTotal PvP deaths
last_pvp_deathstring|nilTimestamp of last PvP death
pvp_kdfloatKill/death ratio
deathsintTotal deaths (all causes)
kill_streakintKills since last death
death_streakintDeaths since last kill
longest_killfloatLongest kill distance in metres
online_streakintConsecutive calendar days with at least one session (including today)

Example output:

{
"pvp_kills": 8,
"last_pvp_kill": "2024-06-19 22:32:23",
"pvp_deaths": 23,
"last_pvp_death": "2024-06-19 22:49:19",
"pvp_kd": 0.35,
"deaths": 58,
"kill_streak": 0,
"death_streak": 2,
"longest_kill": 44.78,
"online_streak": 3
}
Example: Online streak rewardโ€‹

Give a bonus that scales with how many consecutive days the player has been online:

local stats = player.statistics(data.player.username);

if stats.online_streak >= 3 then
local bonus = config.base_reward + (stats.online_streak * config.streak_bonus);
player.change_balance(data.player.username, bonus, "Online-Streak: " .. stats.online_streak .. " Tage");
else
player.change_balance(data.player.username, config.base_reward, "Tรคgliche Belohnung");
end

player.get_faction(username: string)โ€‹

Returns a faction data object for the player's current faction, or nil if the player was not found or is not in a faction.

local faction = player.get_faction("John");

if faction then
print(faction.id); -- e.g. 42
print(faction.name); -- e.g. "Wolves"
end

Return fields:

FieldTypeDescription
idintFaction ID
namestringFaction name
colorstring|nilFaction colour hex code
imagestring|nilFaction image path
balanceint|nilCurrent faction treasury balance
discord_role_idstring|nilLinked Discord role ID
alliestableFactions in an active alliance with this one, as { id, name }; empty when none

Example output:

{
"id": 42,
"name": "Wolves",
"color": "#ff0000",
"image": null,
"balance": 1500,
"discord_role_id": "1234567890",
"allies": [{ "id": 7, "name": "Bears" }]
}
Example: Reward an entire faction on a killโ€‹
local faction = player.get_faction(data.killer.username);

if faction then
faction.change_balance(faction.id, config.faction_reward, "Kill reward");
discord.send_to_channel(config.channel, faction.name .. " earned " .. config.faction_reward .. " for a kill!");
end
note

player.get_faction previously returned the faction ID as a plain string. It now returns an object โ€” use .id to access the ID.

player.get_name(username: string, withFactionTag?: boolean)โ€‹

Resolves the player's display name. If no name is set for the user or no matching user is found, it returns the input username. With true as the second argument the name carries the player's faction tag in the server's format; without it, or when the server shows no tags, the plain name comes back.

player.get_name("johnnyboy420"); -- outputs the display name of the player: "John Doe"
player.get_name("johnnyboy420", true); -- with faction tag: "John Doe [WOLF]"
player.get_faction("nouserwiththatusername"); -- user is not found, so it returns the input: "nouserwiththatusername"

player.get_main_account(username: string)โ€‹

Resolves the main account username for a player. Walks through all players that share at least one device with the given player and returns the username of the first one that has a user account linked. If the player itself has a linked user account, its own username is returned. If no linked account can be found or the player is unknown, the input username is returned unchanged.

player.get_main_account("alt_account"); -- returns "main_account" if the alt shares a device with the main
player.get_main_account("solo_player"); -- returns "solo_player" if no linked main account exists
Example: Credit rewards to the main accountโ€‹
local main = player.get_main_account(data.killer.username);
player.change_balance(main, config.reward or 50, "Kill reward (via " .. data.killer.username .. ")");

player.linked_accounts(username: string)โ€‹

Lists the player's other accounts on this server: every account that logged in from a device this player also used โ€” the same link as player.get_main_account and the alts on the player page. The player themself is not in the list. Returns a list of { id, username }, empty when the player is unknown or has no other accounts.

for _, alt in ipairs(player.linked_accounts(data.player.username)) do
print(data.player.username .. " also plays as " .. alt.username);
end

player.has_discord_role(username: string, role: string)โ€‹

Checks if the player with the given username has the specified Discord role. Accepts both the role name and the role ID.

player.has_discord_role("John", "6553535632356535564"); -- works with role IDs
player.has_discord_role("John", "Admin"); -- works with role names as well

player.ensure_discord_role(username: string, roleId: string, durationSeconds?: number)โ€‹

Grants the specified Discord role to the player. If the player already has the role, nothing happens. The role must be identified by its Discord role ID.

With durationSeconds the role is only lent: dzbot removes it again once the time is up (checked every five minutes). Granting it again while it runs restarts the clock from now and never shortens it, so calling it on every donation keeps the role for that long after the last donation. Granting without a duration makes the role permanent again.

player.ensure_discord_role("John", "1234567890123456789");
player.ensure_discord_role("John", "1234567890123456789", 90 * 86400); -- for 90 days

player.remove_discord_role(username: string, roleId: string)โ€‹

Removes the specified Discord role from the player. If the player does not have the role, nothing happens. The role must be identified by its Discord role ID. A running expiry from ensure_discord_role is dropped with it.

player.remove_discord_role("John", "1234567890123456789");

player.list(filters?: table)โ€‹

Returns a list of all players on the server, optionally filtered. Calling it without arguments returns every player (up to 5000, and logs a warning if more matched); pass page or per_page to walk the result set yourself instead.

local players = player.list();

local vip_players = player.list({ group_name = "vip" });

local active_with_discord = player.list({
has_discord_linked = true,
last_online_after = "-7d",
});

Available filters (all optional, combined with AND):

FilterTypeDescription
searchstringPartial match on username or in-game name
group_idnumberOnly members of the group with this id
group_namestringOnly members of the group with this exact name
whitelistedboolOnly whitelisted / non-whitelisted players
restriction_typesstring or tableOnly players with an ACTIVE restriction of these system types (ban, claim_block)
has_user_accountboolOnly players with / without a linked app account
has_discord_linkedboolOnly players whose linked account has a Discord id
balance_minnumberOnly players whose org-wide balance is at least this value (inclusive)
balance_maxnumberOnly players whose org-wide balance is at most this value (inclusive)
first_seen_afterstringOnly players first seen at or after this time. Relative ("-7d", "-2h", "-1m", "-1y") or an absolute datetime string
first_seen_beforestringOnly players first seen at or before this time. Same format
last_online_afterstringOnly players last seen at or after this time. Same format
last_online_beforestringOnly players last seen at or before this time. Same format
online_nowboolOnly players with / without an open session right now
factionbool or string/numbertrue = in any faction, false = in no faction, or a faction name/id to match a specific faction
variablestring or tableOnly players who have a variable with this name set. Pass a string to match by name only, or a table { name = "...", value = "..." } to also match a specific value
activity_from / activity_tostringWindow for playtime_seconds / session_count. Must be given together
sort_bystringusername, firstSeen, lastOnline, balance, playtime or sessionCount (default lastOnline)
sort_directionstringASC or DESC (default DESC)
page / per_pagenumberExplicit paging. Naming neither returns every match (up to 5000)
group_by_personbooltrue = one entry per person: alts that share a device with a linked account fold into it and share its wallet. Default false = every account

Sorting by playtime or sessionCount requires activity_from and activity_to โ€” without a window the ranking would silently mean "all time".

Return fields (one entry per matching player):

FieldTypeDescription
idintInternal player ID
usernamestringPlayer username
balanceintCurrent org-wide balance
first_onlinestring|nilFirst seen timestamp (Y-m-d H:i:s, UTC)
last_onlinestring|nilLast seen timestamp (Y-m-d H:i:s, UTC)
whitelistedboolWhether the player is whitelisted
bannedboolWhether the player has an active ban restriction
playtime_secondsint|nilPlaytime inside the activity window; nil without a window
session_countint|nilSessions inside the activity window; nil without a window
altstableWith group_by_person: the person's other accounts on this server as { id, username }; empty otherwise

Without group_by_person, every alt of one person is its own entry with the same shared balance. Rank by balance with group_by_person, and use it for per-person rewards too โ€” crediting each alt pays the same wallet several times. With it, last_online, first_online and the activity fields cover all of the person's accounts.

Example: Daily balance reward for all VIP membersโ€‹
local players = player.list({
group_name = "vip",
last_online_after = "-30d",
});

for _, p in ipairs(players) do
player.change_balance(p.username, 100, "Monthly VIP bonus");
end
Example: Notify inactive players via Discordโ€‹
local inactive = player.list({
has_discord_linked = true,
last_online_before = "-14d",
});

for _, p in ipairs(inactive) do
discord.send_to_player(p.username, "We miss you on the server! Come back soon.");
end
Example: The five richest peopleโ€‹
local richest = player.list({ group_by_person = true, sort_by = "balance", sort_direction = "DESC", page = 1, per_page = 5 });

for _, p in ipairs(richest) do
log(p.username .. " (+" .. #p.alts .. " alts): " .. p.balance);
end
Example: Filter by variable valueโ€‹
-- All players whose "vip_tier" variable is set to "gold"
local gold_vips = player.list({ variable = { name = "vip_tier", value = "gold" } });

for _, p in ipairs(gold_vips) do
player.change_balance(p.username, 500, "Gold VIP daily bonus");
end

player.get_variable(username: string, name: string)โ€‹

Returns the value of a player variable, or nil if the variable is not set.

local result = player.get_variable("John", "kill_count");
print(result.value); -- prints the stored value, or nil

Return fields:

FieldTypeDescription
namestringVariable name
valuestring|nilStored value, or nil if not set

player.set_variable(username: string, name: string, value: string|nil)โ€‹

Creates or updates a player variable. Pass nil as value to clear it.

player.set_variable("John", "kill_count", "42");
player.set_variable("John", "vip_tier", "gold");
player.set_variable("John", "temp_flag", nil); -- clears the variable
Example: Track kill streaks with variablesโ€‹
local streak_result = player.get_variable(data.killer.username, "kill_streak");
local streak = tonumber(streak_result.value) or 0;
streak = streak + 1;

player.set_variable(data.killer.username, "kill_streak", tostring(streak));

if streak >= 5 then
player.change_balance(data.killer.username, 250, "Kill streak x" .. streak);
discord.send_to_channel(config.feedChannel, data.killer.username .. " is on a " .. streak .. "-kill streak!");
end

-- Reset the victim's streak
player.set_variable(data.victim.username, "kill_streak", "0");

player.last_position(username: string)โ€‹

Returns the player's last known in-game position, taken from their most recent online ping (recorded roughly every 5 minutes while they are online), or nil if the player is not found or has no recorded position. The position can be minutes or days old โ€” check the time field when freshness matters.

local pos = player.last_position(data.target.username);
if pos then
discord.send_to_channel(config.channel, "Last seen at " .. string.format("%.0f / %.0f", pos.x, pos.z));
end

Return fields:

FieldTypeDescription
xfloatEast/west world coordinate
yfloatHeight
zfloatNorth/south world coordinate
timestringWhen the position was recorded (YYYY-MM-DD HH:MM:SS, UTC)

Faction namespaceโ€‹

The faction namespace contains all faction related action functions.

faction.change_balance(id: string, amount: number, description: string = "")โ€‹

Changes the balance of the faction with the given id.

faction.change_balance("123", 100);
faction.change_balance("123", 100, "Good guys!");
faction.change_balance("123", -100, "Rule penalty!"); -- negative amount to subtract

Bounty namespaceโ€‹

The bounty namespace exposes the bounties of the hitman system.

bounty.list()โ€‹

Returns all open bounties on the server, newest first. Completed or cancelled bounties are never included. Returns an empty list when no bounty is open, so iterating is always safe.

local bounties = bounty.list();
print("Open bounties: " .. #bounties);

Return fields (one entry per bounty):

FieldTypeDescription
idintInternal bounty ID
targetstring|nilUsername of the player the bounty is on; nil for a bounty on a faction
target_idint|nilInternal player ID of the target
target_identifierstring|nilGame identifier of the target (the identifier in event data)
target_factionstring|nilName of the faction a faction bounty is on; nil for a bounty on a player
target_faction_idint|nilID of that faction
created_bystring|nilUsername of the player who placed and paid for the bounty; nil when the server funds it
server_fundedbooltrue for a bounty an action placed with bounty.create without funded_by
amountintBounty amount
reasonstring|nilReason given when placing the bounty
target_all_accountsboolWhether killing any account linked to the target's device pays out (always true for a faction bounty)
allies_may_collectboolWhether faction mates and allies of the target may collect it
created_atstringWhen the bounty was placed (YYYY-MM-DD HH:MM:SS, UTC)

Several bounties can exist on the same target โ€” group by target yourself if you need one entry per player. A bounty on a faction has target_faction set and target nil.

Example: Scheduled bounty status postโ€‹

Post the last known position of every player with an open bounty, e.g. from a scheduled custom action. No message is sent when no bounty is open:

local bounties = bounty.list();
if #bounties > 0 then
local lines = {};
for _, b in ipairs(bounties) do
local pos = b.target and player.last_position(b.target);
local where = pos and string.format("%.0f / %.0f", pos.x, pos.z) or "unknown";
table.insert(lines, (b.target or ("faction " .. b.target_faction)) .. " (" .. b.amount .. ") โ€” last seen: " .. where);
end
discord.send_to_channel(config.channel, "**Open bounties:**\n" .. table.concat(lines, "\n"));
end

bounty.create(options: table)โ€‹

Places a bounty on a player, exactly like one a player places on the website: it appears in the bounty list and on the map, fires the bounty_placed event (so an installed announcement action posts it) and pays the hitman share to whoever kills the target โ€” by default also when one of the target's linked accounts is killed.

With target_faction instead of target the bounty is on a whole faction: the first kill of any member collects it, members (and players with a linked account in the faction) cannot, and it fires faction_bounty_placed instead of bounty_placed โ€” see Bounties on a faction.

OptionTypeDescription
targetint|stringInternal player ID, game identifier (data.killer.identifier) or username
target_factionint|stringInstead of target: the faction, by ID, name or tag. Pass exactly one of the two
amountintRequired. Whole number greater than 0. The minimum bounty amount of the server settings does not apply to scripts
reasonstringOptional text shown with the bounty (up to 255 characters)
target_all_accountsboolAlso pay out when a linked account of the target is killed (default true; bounties on a player only)
allies_may_collectboolFaction mates and allies of the target may collect too (default false) โ€” see Allies and faction mates
funded_byint|stringOptional player who pays the amount (same forms as target). Cannot be the target or one of their linked accounts โ€” nor, for a faction bounty, a member of the faction or have a linked account in it. Unless allies_may_collect is true, they also cannot be allied with the target faction or with a faction of the target player

Who pays for it:

  • Without funded_by the server funds the bounty. Nothing is debited when it is placed. The hitman's payout is new money and shows in their wallet history as Server bounty reward for eliminating โ€ฆ. Withdrawing such a bounty refunds nobody. In the admin bounty list it shows Server (automation) as the one who placed it.
  • With funded_by that player pays the amount up front, as if they had placed the bounty themselves. If they cannot afford it, the script stops with Not enough balance.

An unknown player or faction, an invalid amount, or passing both or neither of target and target_faction also stops the script with an error.

Returns a table with id, code, target, target_id, target_faction, target_faction_id (the target fields of the other kind are nil), amount, reason, target_all_accounts, allies_may_collect, server_funded and created_by (nil when the server funds it).

-- a kill at the airfield puts a bounty of 500 on the killer, paid by the server
for _, zone in ipairs(data.zones) do
if zone.name == "NWAF" then
local b = bounty.create({
target = data.killer.identifier,
amount = 500,
reason = "Killed " .. data.victim.username .. " at the NWAF",
});
print("Bounty " .. b.code .. " placed on " .. b.target);
break;
end
end

-- the victim pays for revenge out of their own wallet
bounty.create({ target = data.killer.username, amount = 250, funded_by = data.victim.username });

-- a server-funded bounty of 2000 on a whole faction, by its tag
bounty.create({ target_faction = "ALPHA", amount = 2000, reason = "Raided the trader" });

The ready-made Auto Bounty action in the marketplace does this for you: a bounty on every player who kills another player, optionally only inside the zones you pick, with a cooldown, a limit of open auto bounties per player and an optional Discord post.

Bookmaker namespaceโ€‹

The bookmaker namespace exposes the bets of the bookmaker. Players join an open bet in Discord with /bet <code>, using the bet's four-character code.

bookmaker.list()โ€‹

Returns the bets on the server that are open for entries or waiting for their result, newest first. Settled and cancelled bets are never included. Returns an empty list when there are none, so iterating is always safe.

FieldTypeDescription
idintInternal bet ID
codestringFour-character bet code โ€” what players pass to /bet <code>
titlestringBet title
statusstring"open" or "waiting_for_result"
deadlinestring|nilEntry deadline (YYYY-MM-DD HH:MM:SS, server time)
bookmakerstring|nilUsername of the bookmaker
total_potintSum of all stakes so far
outcome_countintNumber of outcomes
Example: Scheduled post of open betsโ€‹
local lines = {};
for _, b in ipairs(bookmaker.list()) do
if b.status == "open" then
table.insert(lines, "**#" .. b.code .. "** " .. b.title .. " (pot: " .. b.total_pot .. ") โ†’ `/bet " .. b.code .. "`");
end
end
if #lines > 0 then
discord.send_to_channel(config.channel, "**Open bets:**\n" .. table.concat(lines, "\n"));
end

bookmaker.get(id: int)โ€‹

Returns one bet of this server in any status, or nil if there is no bet with that ID on this server.

The stakes per outcome are always returned, even for bets that hide their odds from players โ€” the action belongs to the server owner. Check odds_visible before posting them where players can see them.

FieldTypeDescription
idintInternal bet ID
codestringFour-character bet code
titlestringBet title
descriptionstring|nilBet description
statusstring"open", "waiting_for_result", "settled" or "cancelled"
deadlinestring|nilEntry deadline (YYYY-MM-DD HH:MM:SS, server time)
min_bet / max_betint|nilStake limits of the bet, nil = server default
odds_visibleboolWhether players see the stakes per outcome
bookmakerstring|nilUsername of the bookmaker
total_potintSum of all stakes
outcomestableList of { id, label, total_amount, entry_count, is_winner } โ€” is_winner is nil until the bet is settled

Map namespaceโ€‹

The map namespace turns raw coordinates into something players can read and click.

map.locate(x: number, z: number)โ€‹

Returns the nearest named place on the server's map and an iZurvive link to the position โ€” the same text and link as the map links on the website. The map follows the server's mission (Chernarus, Livonia, Sakhal, Nasdara). Never returns nil.

local loc = map.locate(data.location.x, data.location.z);
discord.send_to_channel(config.channel, "Spotted near [" .. loc.label .. "](" .. loc.url .. ")");

Return fields:

FieldTypeDescription
placestring|nilNearest named place, nil when none is within 1.5 km
labelstringplace, or the rounded coordinates when there is none
urlstringiZurvive link centred on the position

map.players_near(x: number, z: number, radius: number, from?: number, to?: number)โ€‹

Lists every player whose logged position was within radius metres (at most 2000) of the spot between from and to (Unix timestamps, e.g. data.time.timestamp). Without them it looks at the last 10 minutes; the window may span at most 24 hours. Every log line with a position counts โ€” the online ping every 5 minutes, but also hits, placing, building or emotes โ€” so a player who stayed only a moment and did nothing logged can be missing. Lines from after an event may not be imported yet when the action runs. Returns a list, nearest first, never nil.

local t = data.time.timestamp;
for _, p in ipairs(map.players_near(data.location.x, data.location.z, 100, t - 600, t)) do
print(p.username .. " was " .. math.floor(p.distance) .. " m away at " .. p.time);
end

Return fields per player:

FieldTypeDescription
usernamestringPlayer username
distancefloatMetres from the spot at the closest logged moment
x / zfloatWhere the player was at that moment
timestringWhen that was (YYYY-MM-DD HH:MM:SS, UTC)

map.base_at(x: number, z: number)โ€‹

Returns the base standing at the spot, or nil when no known base covers it. A base covers about 60 m around each of its building spots; when several do, the nearest one that is not gone wins.

local base = map.base_at(data.location.x, data.location.z);
if base then
print(data.player.username .. " is at " .. base.name .. " (" .. #base.contributors .. " builders)");
end

Return fields:

FieldTypeDescription
codestringThe base's code, as shown in the admin panel
namestringDisplay name (set by hand, or generated from its owner)
statusstringactive, decaying or gone
ownerstring|nilUsername of the leading builder
factiontable|nil{ id, name } when one faction did at least 75 % of the work
contributorstableEveryone who built there, strongest first: { username, share, events } โ€” share is their part of the work (0 to 1), events how many of their build, place or flag actions were logged there

Somebody who placed a single item at a base is a contributor too, with a tiny share and events of 1. Check events or share when only the real builders should count.

map.image(options: table)โ€‹

Draws an 800 ร— 600 picture of the server's map (Chernarus, Livonia, Sakhal or Nasdara, following the mission) with labelled markers and an optional route, and returns an image you pass to discord.send_to_channel or discord.send_to_player. The picture fits itself around every point and the whole route, at most as close as about 1.5 km across.

OptionTypeDescription
pointstableMarkers: a list of { x = number, z = number, label = "text", color = "red" } (label and color optional), up to 25
routetableOptional line through a list of { x, z } positions in order, up to 500 โ€” green dot at the start, red at the end
zoomnumberOptional fixed zoom: 1 shows the whole map, 8 is the closest

Colors: red (default), blue, green, yellow, orange, purple, grey, white, black, or "#rrggbb". Labels are cut at 40 characters. A point without numeric x and z stops the action with an error, so check positions that can be missing first.

Return fields:

FieldTypeDescription
typestringAlways "map_image"
idstringIdentifies the image; the same input gives the same id
zoomnumberZoom the picture was drawn at
width, heightnumberSize in pixels
render_msnumberHow long drawing took, 0 when the same picture was drawn in the last 15 minutes

An action can draw up to 5 images per run. Drawing takes a few hundred milliseconds, up to a few seconds when the map area has not been drawn for a while; the action log shows the time of each image.

Example: Kill location (player_kill)โ€‹
local img = map.image({
points = {{ x = data.location.x, z = data.location.z, label = data.victim.username }},
});
discord.send_to_channel(config.channel, data.killer.username .. " killed " .. data.victim.username .. " with " .. data.weapon, img);
Example: Bounty target spotted (player_online)โ€‹

The map appears inside the embed:

for _, b in ipairs(bounty.list()) do
if b.target_identifier == data.player.identifier then
local img = map.image({
points = {{ x = data.location.x, z = data.location.z, label = b.target .. " (" .. b.amount .. ")", color = "orange" }},
});
discord.send_to_channel(config.channel, "<embed><title>Bounty target spotted</title><description>"
.. b.target .. " near " .. map.locate(data.location.x, data.location.z).label .. "</description></embed>", img);
break;
end
end
Example: Base alarm (base_alarm_created)โ€‹
local img = map.image({ points = {{ x = data.posX, z = data.posZ, label = data.faction.name, color = "yellow" }}, zoom = 6 });
discord.send_to_channel(config.channel, "Base alarm of " .. data.faction.name .. " triggered by " .. data.player.username, img);
Example: Route with a destination, as a DMโ€‹
local img = map.image({
route = {{ x = 4500, z = 10200 }, { x = 5200, z = 9800 }, { x = 6100, z = 9300 }},
points = {{ x = 6100, z = 9300, label = "Meeting point", color = "green" }},
});
discord.send_to_player(data.player.username, "Your route to the meeting point", img);

Math namespaceโ€‹

The math namespace contains utility functions for calculations.

math.distance(x1: number, z1: number, x2: number, z2: number)โ€‹

Calculates the 2D distance between two points. Useful for checking if a position is within a certain radius of another position.

-- Calculate distance between two coordinates
local dist = math.distance(1000, 2000, 1500, 2500);
print("Distance: " .. dist); -- outputs approximately 707.1
Example: Radius checkโ€‹

Check if a kill happened within a certain radius of a location (e.g., a safezone or base):

local safezone_x = 5000;
local safezone_z = 5000;
local safezone_radius = 100;

local kill_distance = math.distance(data.x, data.z, safezone_x, safezone_z);

if kill_distance < safezone_radius then
discord.send_to_channel(config.alertChannel, "Kill detected inside safezone!");
server.ban_player(data.killer, "Killing in safezone", "1 day");
end
Example: Reward long-distance killsโ€‹
-- Calculate distance between killer and victim
local kill_range = math.distance(data.killerX, data.killerZ, data.victimX, data.victimZ);

if kill_range > 500 then
player.change_balance(data.killer, 100, "Long range kill bonus (" .. math.floor(kill_range) .. "m)");
discord.send_to_channel(config.feedChannel, data.killer .. " scored a " .. math.floor(kill_range) .. "m kill!");
end

HTTP namespaceโ€‹

The http namespace allows actions to make outbound HTTP requests to external services. Requests have a 5-second timeout.

http.get(url: string, headers?: table)โ€‹

Performs an HTTP GET request. Returns two values: the HTTP status code and the response body as a string.

local status, body = http.get("https://api.example.com/data");
print(status); -- e.g. 200
print(body); -- raw response body string

With custom headers:

local status, body = http.get("https://api.example.com/data", {
["Authorization"] = "Bearer mytoken",
["Accept"] = "application/json",
});

http.post(url: string, body?: string, headers?: table)โ€‹

Performs an HTTP POST request. Returns two values: the HTTP status code and the response body as a string.

local status, body = http.post("https://api.example.com/webhook", "payload=hello");

Sending a JSON body:

local payload = json.encode({ event = "kill", player = data.killer.username });
local status, body = http.post("https://api.example.com/events", payload, {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer mytoken",
});
Example: Notify an external webhook on a kill eventโ€‹
local payload = json.encode({
killer = data.killer.username,
victim = data.victim.username,
weapon = data.weapon,
});

local status, body = http.post(config.webhookUrl, payload, {
["Content-Type"] = "application/json",
});

if status ~= 200 then
print("Webhook failed with status: " .. status);
end

JSON namespaceโ€‹

The json namespace provides JSON encoding and decoding to work with API responses and payloads.

json.encode(value)โ€‹

Encodes a Lua value (table, string, number, boolean) as a JSON string.

local encoded = json.encode({ name = "John", score = 42 });
print(encoded); -- {"name":"John","score":42}

json.decode(json: string)โ€‹

Parses a JSON string and returns a Lua table.

local decoded = json.decode('{"name":"John","score":42}');
print(decoded.name); -- John
print(decoded.score); -- 42
Example: Call a JSON API and use the responseโ€‹
local status, body = http.get("https://api.example.com/players/" .. data.player.username);

if status == 200 then
local result = json.decode(body);
if result.vip == true then
player.change_balance(data.player.username, config.vipBonus, "VIP bonus");
end
end

Scheduled actionsโ€‹

The trigger Scheduled is not an event at all: an action with it runs only when a scheduler task Run action runs it, at the times you set in the task. Use it for anything that should happen on the clock โ€” a weekly wage, a daily post, a monthly clean-up โ€” while its settings stay in the action's form, where staff without code access can change them.

Such an action gets its config as usual. data describes the run:

FieldTypeDescription
data.time.stringstringWhen the action ran (server time, Y-m-d H:i:s)
data.time.timestampintThe same as a Unix timestamp
data.scheduled_at.stringstringWhen the task was due. A run starts a few seconds after that; for Run now it is the moment of the click
data.scheduled_at.timestampintThe same as a Unix timestamp
data.task_idintID of the scheduler task
data.task_descriptionstring|nilThe task's description
data.recurringbooltrue for a recurring task, false for a one-off one

A task can run an action more often than you planned โ€” Run now, a duplicated task. If the action pays out, let it remember what it already paid, for example with a player variable (player.set_variable) per day:

local day = string.sub(data.scheduled_at.string, 1, 10);
for _, p in ipairs(player.list({ group_name = config.group })) do
if player.get_variable(p.username, "bonus_paid").value ~= day then
player.set_variable(p.username, "bonus_paid", day);
player.change_balance(p.username, config.amount, "Daily bonus " .. day);
end
end

A disabled action is skipped, and the task moves on to its next run. A scheduled action has no game event to react to, so it never runs on its own.

In-app eventsโ€‹

Most action triggers come directly from game server logs (kills, connections, base building, etc.). A small number of events are generated by dzbot itself when something happens inside the platform. These synthetic events go through the same action pipeline as game log events.

bounty_placedโ€‹

Fires whenever a bounty goes live โ€” placed by a player via the shop, or by an action with bounty.create. Use this trigger to notify the target, post to a Discord channel, or run any other automation when a bounty goes live.

Event payload:

FieldTypeDescription
data.target.usernamestringUsername of the player with the bounty on their head
data.target.identifierstringSteam/DayZ ID of the target
data.placedBy.usernamestringUsername of the player who placed the bounty; the server name when the server funds it
data.placedBy.identifierstringSteam/DayZ ID of the placer; empty when the server funds it
data.serverFundedbooltrue when an action placed the bounty without a paying player
data.amountintBounty size in server currency
data.reasonstring|nilOptional reason entered by the placer
data.bountyIdint|nilInternal bounty ID

Example โ€” DM the targeted player:

local reason = data.reason or "No reason given";
discord.send_to_player(
data.target.username,
"A bounty of **" .. data.amount .. "** has been placed on your head by "
.. data.placedBy.username .. ".\n> " .. reason
);

Example โ€” Post to a channel:

local channelId = config.channel or "";
if channelId == "" then return end;

local reason = data.reason or "No reason given";
discord.send_to_channel(
channelId,
"**Bounty placed!** " .. discord.mention(data.placedBy.username)
.. " put a bounty of **" .. data.amount .. "** on "
.. discord.mention(data.target.username) .. ".\n> *" .. reason .. "*"
);

faction_bounty_placedโ€‹

Fires when a bounty on a whole faction goes live โ€” placed by a player, or by an action with bounty.create({ target_faction = โ€ฆ }). A bounty on a player fires bounty_placed instead, so an announcement action for player bounties does not post these.

FieldTypeDescription
data.faction.idintID of the faction
data.faction.namestringName of the faction
data.faction.tagstring|nilTag of the faction
data.memberslistThe members when the bounty was placed, each with username and identifier
data.placedBy.usernamestringWho placed it; the server name when the server funds it
data.placedBy.identifierstringSteam/DayZ ID of the placer; empty when the server funds it
data.serverFundedbooltrue when an action placed it without a paying player
data.amountintBounty size in server currency
data.reasonstring|nilOptional reason
data.bountyIdintInternal bounty ID

Example โ€” post to a channel and DM every member:

local reason = data.reason or "No reason given";
discord.send_to_channel(config.channel,
"**Bounty on a faction!** " .. data.placedBy.username .. " put **" .. data.amount
.. "** on " .. data.faction.name .. ". The first kill of any member collects it.\n> *" .. reason .. "*");
for _, member in ipairs(data.members) do
discord.send_to_player(member.username, "Your faction " .. data.faction.name .. " has a bounty of " .. data.amount .. " on it.");
end

bounty_completedโ€‹

Fires when a kill collects a bounty โ€” on a player or on a faction. The event's player is the hitman.

FieldTypeDescription
data.hitman.username / .identifierstringThe killer who collected it
data.victim.username / .identifierstringThe player who was killed
data.targettable|nilFor a bounty on a player: username and identifier of the account it was placed on (the victim, or the account of theirs it named); nil for a faction bounty
data.targetFactiontable|nilFor a bounty on a faction: id, name, tag; nil for a player bounty
data.placedBy.username / .identifierstringWho placed it; the server name and an empty identifier when the server funded it
data.serverFundedbooltrue when the server funded it
data.amountintThe bounty's amount
data.payoutintWhat the hitman received (the hitman cut of the amount)
data.bountyIdintInternal bounty ID
local on = data.targetFaction and ("the faction " .. data.targetFaction.name) or data.target.username;
discord.send_to_channel(config.channel,
"**Bounty collected!** " .. data.hitman.username .. " killed " .. data.victim.username
.. " and collected " .. data.payout .. " for the bounty on " .. on .. ".");

faction_alliance_formed, faction_alliance_joined, faction_alliance_left, faction_alliance_dissolvedโ€‹

Fire when an alliance changes. They are not tied to a player and post nothing on their own โ€” install or write an action to announce them.

EventWhendata.faction is
faction_alliance_formedThe first invited faction accepted, so the alliance now holdsthe faction that accepted
faction_alliance_joinedA further faction joined after every member agreedthe new member
faction_alliance_leftA faction left an alliance that goes on with two or more factionsthe faction that left
faction_alliance_dissolvedFewer than two factions are left, so the alliance endedthe faction that left last
FieldTypeDescription
data.alliance.idintID of the alliance
data.alliance.namestringName of the alliance
data.faction.id / .name / .tagint / string / string|nilThe faction the change was about (see above)
data.factionslistThe member factions after the change, each with id, name and tag. For faction_alliance_dissolved, the one faction that was still in it
data.reasonstringfaction_alliance_left only: left (it left), removed (the other members voted it out) or faction_deleted

Example โ€” announce a new member (trigger faction_alliance_joined):

local names = {};
for _, f in ipairs(data.factions) do
table.insert(names, f.name);
end
discord.send_to_channel(config.channel,
"๐Ÿค " .. data.faction.name .. " joined **" .. data.alliance.name .. "**. Members now: " .. table.concat(names, ", "));

bet_opened, bet_closed, bet_settled, bet_cancelledโ€‹

Fire along the lifecycle of a bookmaker bet โ€” whether a player, an admin or an event match created it. Use them to announce new bets in a Discord channel, remind players when betting closes, or post the result. data.bookmaker is the player who runs the bet; bookmaker.get(data.bet_id) returns the full bet.

EventWhen
bet_openedA bet was created
bet_closedA bet stopped taking entries and waits for its result โ€” closed by hand or because its deadline passed
bet_settledThe winning outcome was set and the winners were paid out
bet_cancelledA bet was cancelled and every entry refunded

Event payload (all four):

FieldTypeDescription
data.bookmaker.usernamestringUsername of the bookmaker
data.bookmaker.identifierstringSteam/DayZ ID of the bookmaker
data.bet_idintInternal bet ID, for bookmaker.get(data.bet_id)
data.bet_codestringFour-character bet code โ€” what players pass to /bet <code>
data.titlestringBet title

Additional fields:

EventFieldTypeDescription
bet_openeddata.descriptionstring|nilBet description
bet_openeddata.outcomestableList of { id, label }
bet_openeddata.deadlinestring|nilEntry deadline (YYYY-MM-DD HH:MM:SS, server time)
bet_openeddata.min_bet / data.max_betint|nilStake limits, nil = server default
bet_closeddata.reasonstring"manual" or "deadline"
bet_closeddata.total_potintSum of all stakes
bet_closeddata.entry_countintNumber of entries
bet_settleddata.winning_outcome.id / .labelint / stringThe winning outcome
bet_settleddata.total_potintSum of all stakes
bet_settleddata.winner_countintPlayers who were paid out
bet_settleddata.total_payoutintSum paid out to the winners
bet_cancelleddata.refunded_totalintSum refunded
bet_cancelleddata.entry_countintNumber of entries

Example โ€” Announce a new bet (trigger bet_opened):

local options = {};
for _, o in ipairs(data.outcomes) do
table.insert(options, "โ€ข " .. o.label);
end
local deadline = data.deadline and ("\nBetting closes " .. data.deadline) or "";
discord.send_to_channel(
config.channel,
"**New bet #" .. data.bet_code .. ": " .. data.title .. "**\n" .. table.concat(options, "\n")
.. deadline .. "\nJoin with `/bet " .. data.bet_code .. "`"
);

Example โ€” Post the result (trigger bet_settled):

discord.send_to_channel(
config.channel,
"**" .. data.title .. "** is decided: " .. data.winning_outcome.label .. " wins. "
.. data.winner_count .. " winners share " .. data.total_payout .. " " .. server.currency() .. "."
);

discord_member_joinedโ€‹

Fires when somebody joins the Discord server your game server is connected to โ€” the trigger for a welcome message. It fires for every member who joins, whether or not they play on your server yet; bots never trigger it. If several game servers share one Discord, it fires on each of them.

Event payload:

FieldTypeDescription
data.mentionstringMention text (<@id>) โ€” put it into a message to ping the new member
data.discord_idstringDiscord user ID
data.discord_usernamestringDiscord username
data.discord_namestringDisplay name, or the username if none is set
data.discord_created_atstring|nilWhen the Discord account was created (ISO 8601)
data.usernamestring|nilIn-game username on this server โ€” nil until the member links a game account
data.rejoinedbooltrue when the member was already linked before this join, i.e. a returning member

Example โ€” Welcome new members in a channel:

if data.rejoined then
discord.send_to_channel(config.channel, "Welcome back, " .. data.mention .. "!");
return;
end

discord.send_to_channel(
config.channel,
"Welcome to " .. server.name() .. ", " .. data.mention .. "! Read the rules and link your game account with `/link`."
);

The ready-made Discord Welcome Message action in the marketplace does this with a configurable channel and text.

player_warnedโ€‹

Fires when a player receives a warning โ€” from your team (website, Discord player card, AI companion) or automatically for violating a restriction that counts violations as warnings. Warning stages ban on their own; use this event for anything else, such as a post in your staff channel.

Event payload:

FieldTypeDescription
data.player.usernamestringThe warned player
data.noteIdnumberThe warning note
data.reasonstringWhat the warning is for
data.warnedBystringUsername of the staff member โ€” empty for an automatic warning
data.automaticbooltrue when a restriction violation gave the warning
data.restrictionIdnumber|nilThe restriction, for an automatic warning
data.activeWarningsnumberThe player's warnings on this server that have not expired, this one included
data.expiresAtstring|nilWhen the warning expires (ISO 8601), nil if never

Example โ€” Tell the staff channel:

discord.send_to_channel(
config.channel,
"โš ๏ธ " .. data.player.username .. " was warned (" .. data.activeWarnings .. " active): " .. data.reason
);