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.
Navigation pathโ
Server Admin->Actions
Tabsโ
The actions screen is split into three tabs:
Marketplace: browse public creator actions and install them on the current serverInstalled: manage marketplace actions already installed on this serverCustom 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_killsvariable, 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_streakandfeed_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 eventconfig: 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:
nilif the field is hidden by its condition or its section- Otherwise the saved value from Settings tab (if present)
- Otherwise the field default from schema
- Otherwise
nil
Only fields defined in the schema are exposed in config.
Field typesโ
| Type | Lua value | Notes |
|---|---|---|
string | string|nil | Single-line text; empty โ nil |
textarea | string|nil | Multi-line text; identical to string in Lua |
email | string|nil | Email address input; identical to string in Lua |
number | number|nil | Parsed as int or float |
boolean | bool|nil | Checkbox; empty โ nil |
coordinate | table|nil | Map picker โ access .x, .z, optionally .y, .radius |
generic_select | string|nil or string[] | Dropdown from a fixed list; string[] when multiple: true |
faction_select | string|nil or string[] | Server's faction IDs |
player_select | string|nil or string[] | Server's player usernames |
zone_select | string|nil or string[] | Server's zone IDs โ compare with tostring(zone.id) from data.zones |
relation_select | string[] | Always an array; never nil |
discord_channel_select | string|nil or string[] | Discord text channel IDs |
discord_category_select | string|nil or string[] | Discord category IDs |
discord_role_select | string|nil or string[] | Discord role IDs |
discord_guild_select | string|nil or string[] | Discord guild IDs |
object_list | table[] | Repeatable list of objects; never nil โ empty returns {} |
event_filter | table | Conditions picked in a form โ pass it to filter.matches(config.<name>, data). Actions only |
discord_embed | table|nil | A 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 โ
mosinmatches 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:
| Name | Type | Default | Description |
|---|---|---|---|
| channelId | discord_channel_select | Discord channel for notifications | |
| notifyOnHeadshot | boolean | true | Send extra message for headshots |
| minDistanceForBonus | number | 200 | Minimum distance for bonus reward |
| bonusAmount | number | 50 | Currency 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:
Print debug outputโ
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:
| Field | Type | Description |
|---|---|---|
pvp_kills | int | Total PvP kills |
last_pvp_kill | string|nil | Timestamp of last kill |
pvp_deaths | int | Total PvP deaths |
last_pvp_death | string|nil | Timestamp of last PvP death |
pvp_kd | float | Kill/death ratio |
deaths | int | Total deaths (all causes) |
kill_streak | int | Kills since last death |
death_streak | int | Deaths since last kill |
longest_kill | float | Longest kill distance in metres |
online_streak | int | Consecutive 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:
| Field | Type | Description |
|---|---|---|
id | int | Faction ID |
name | string | Faction name |
color | string|nil | Faction colour hex code |
image | string|nil | Faction image path |
balance | int|nil | Current faction treasury balance |
discord_role_id | string|nil | Linked Discord role ID |
allies | table | Factions 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
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):
| Filter | Type | Description |
|---|---|---|
search | string | Partial match on username or in-game name |
group_id | number | Only members of the group with this id |
group_name | string | Only members of the group with this exact name |
whitelisted | bool | Only whitelisted / non-whitelisted players |
restriction_types | string or table | Only players with an ACTIVE restriction of these system types (ban, claim_block) |
has_user_account | bool | Only players with / without a linked app account |
has_discord_linked | bool | Only players whose linked account has a Discord id |
balance_min | number | Only players whose org-wide balance is at least this value (inclusive) |
balance_max | number | Only players whose org-wide balance is at most this value (inclusive) |
first_seen_after | string | Only players first seen at or after this time. Relative ("-7d", "-2h", "-1m", "-1y") or an absolute datetime string |
first_seen_before | string | Only players first seen at or before this time. Same format |
last_online_after | string | Only players last seen at or after this time. Same format |
last_online_before | string | Only players last seen at or before this time. Same format |
online_now | bool | Only players with / without an open session right now |
faction | bool or string/number | true = in any faction, false = in no faction, or a faction name/id to match a specific faction |
variable | string or table | Only 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_to | string | Window for playtime_seconds / session_count. Must be given together |
sort_by | string | username, firstSeen, lastOnline, balance, playtime or sessionCount (default lastOnline) |
sort_direction | string | ASC or DESC (default DESC) |
page / per_page | number | Explicit paging. Naming neither returns every match (up to 5000) |
group_by_person | bool | true = 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):
| Field | Type | Description |
|---|---|---|
id | int | Internal player ID |
username | string | Player username |
balance | int | Current org-wide balance |
first_online | string|nil | First seen timestamp (Y-m-d H:i:s, UTC) |
last_online | string|nil | Last seen timestamp (Y-m-d H:i:s, UTC) |
whitelisted | bool | Whether the player is whitelisted |
banned | bool | Whether the player has an active ban restriction |
playtime_seconds | int|nil | Playtime inside the activity window; nil without a window |
session_count | int|nil | Sessions inside the activity window; nil without a window |
alts | table | With 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:
| Field | Type | Description |
|---|---|---|
name | string | Variable name |
value | string|nil | Stored 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:
| Field | Type | Description |
|---|---|---|
x | float | East/west world coordinate |
y | float | Height |
z | float | North/south world coordinate |
time | string | When 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):
| Field | Type | Description |
|---|---|---|
id | int | Internal bounty ID |
target | string|nil | Username of the player the bounty is on; nil for a bounty on a faction |
target_id | int|nil | Internal player ID of the target |
target_identifier | string|nil | Game identifier of the target (the identifier in event data) |
target_faction | string|nil | Name of the faction a faction bounty is on; nil for a bounty on a player |
target_faction_id | int|nil | ID of that faction |
created_by | string|nil | Username of the player who placed and paid for the bounty; nil when the server funds it |
server_funded | bool | true for a bounty an action placed with bounty.create without funded_by |
amount | int | Bounty amount |
reason | string|nil | Reason given when placing the bounty |
target_all_accounts | bool | Whether killing any account linked to the target's device pays out (always true for a faction bounty) |
allies_may_collect | bool | Whether faction mates and allies of the target may collect it |
created_at | string | When 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.
| Option | Type | Description |
|---|---|---|
target | int|string | Internal player ID, game identifier (data.killer.identifier) or username |
target_faction | int|string | Instead of target: the faction, by ID, name or tag. Pass exactly one of the two |
amount | int | Required. Whole number greater than 0. The minimum bounty amount of the server settings does not apply to scripts |
reason | string | Optional text shown with the bounty (up to 255 characters) |
target_all_accounts | bool | Also pay out when a linked account of the target is killed (default true; bounties on a player only) |
allies_may_collect | bool | Faction mates and allies of the target may collect too (default false) โ see Allies and faction mates |
funded_by | int|string | Optional 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_bythe 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_bythat player pays the amount up front, as if they had placed the bounty themselves. If they cannot afford it, the script stops withNot 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.
| Field | Type | Description |
|---|---|---|
id | int | Internal bet ID |
code | string | Four-character bet code โ what players pass to /bet <code> |
title | string | Bet title |
status | string | "open" or "waiting_for_result" |
deadline | string|nil | Entry deadline (YYYY-MM-DD HH:MM:SS, server time) |
bookmaker | string|nil | Username of the bookmaker |
total_pot | int | Sum of all stakes so far |
outcome_count | int | Number 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.
| Field | Type | Description |
|---|---|---|
id | int | Internal bet ID |
code | string | Four-character bet code |
title | string | Bet title |
description | string|nil | Bet description |
status | string | "open", "waiting_for_result", "settled" or "cancelled" |
deadline | string|nil | Entry deadline (YYYY-MM-DD HH:MM:SS, server time) |
min_bet / max_bet | int|nil | Stake limits of the bet, nil = server default |
odds_visible | bool | Whether players see the stakes per outcome |
bookmaker | string|nil | Username of the bookmaker |
total_pot | int | Sum of all stakes |
outcomes | table | List 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:
| Field | Type | Description |
|---|---|---|
place | string|nil | Nearest named place, nil when none is within 1.5 km |
label | string | place, or the rounded coordinates when there is none |
url | string | iZurvive 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:
| Field | Type | Description |
|---|---|---|
username | string | Player username |
distance | float | Metres from the spot at the closest logged moment |
x / z | float | Where the player was at that moment |
time | string | When 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:
| Field | Type | Description |
|---|---|---|
code | string | The base's code, as shown in the admin panel |
name | string | Display name (set by hand, or generated from its owner) |
status | string | active, decaying or gone |
owner | string|nil | Username of the leading builder |
faction | table|nil | { id, name } when one faction did at least 75 % of the work |
contributors | table | Everyone 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.
| Option | Type | Description |
|---|---|---|
points | table | Markers: a list of { x = number, z = number, label = "text", color = "red" } (label and color optional), up to 25 |
route | table | Optional line through a list of { x, z } positions in order, up to 500 โ green dot at the start, red at the end |
zoom | number | Optional 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:
| Field | Type | Description |
|---|---|---|
type | string | Always "map_image" |
id | string | Identifies the image; the same input gives the same id |
zoom | number | Zoom the picture was drawn at |
width, height | number | Size in pixels |
render_ms | number | How 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:
| Field | Type | Description |
|---|---|---|
data.time.string | string | When the action ran (server time, Y-m-d H:i:s) |
data.time.timestamp | int | The same as a Unix timestamp |
data.scheduled_at.string | string | When 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.timestamp | int | The same as a Unix timestamp |
data.task_id | int | ID of the scheduler task |
data.task_description | string|nil | The task's description |
data.recurring | bool | true 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:
| Field | Type | Description |
|---|---|---|
data.target.username | string | Username of the player with the bounty on their head |
data.target.identifier | string | Steam/DayZ ID of the target |
data.placedBy.username | string | Username of the player who placed the bounty; the server name when the server funds it |
data.placedBy.identifier | string | Steam/DayZ ID of the placer; empty when the server funds it |
data.serverFunded | bool | true when an action placed the bounty without a paying player |
data.amount | int | Bounty size in server currency |
data.reason | string|nil | Optional reason entered by the placer |
data.bountyId | int|nil | Internal 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.
| Field | Type | Description |
|---|---|---|
data.faction.id | int | ID of the faction |
data.faction.name | string | Name of the faction |
data.faction.tag | string|nil | Tag of the faction |
data.members | list | The members when the bounty was placed, each with username and identifier |
data.placedBy.username | string | Who placed it; the server name when the server funds it |
data.placedBy.identifier | string | Steam/DayZ ID of the placer; empty when the server funds it |
data.serverFunded | bool | true when an action placed it without a paying player |
data.amount | int | Bounty size in server currency |
data.reason | string|nil | Optional reason |
data.bountyId | int | Internal 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.
| Field | Type | Description |
|---|---|---|
data.hitman.username / .identifier | string | The killer who collected it |
data.victim.username / .identifier | string | The player who was killed |
data.target | table|nil | For 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.targetFaction | table|nil | For a bounty on a faction: id, name, tag; nil for a player bounty |
data.placedBy.username / .identifier | string | Who placed it; the server name and an empty identifier when the server funded it |
data.serverFunded | bool | true when the server funded it |
data.amount | int | The bounty's amount |
data.payout | int | What the hitman received (the hitman cut of the amount) |
data.bountyId | int | Internal 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.
| Event | When | data.faction is |
|---|---|---|
faction_alliance_formed | The first invited faction accepted, so the alliance now holds | the faction that accepted |
faction_alliance_joined | A further faction joined after every member agreed | the new member |
faction_alliance_left | A faction left an alliance that goes on with two or more factions | the faction that left |
faction_alliance_dissolved | Fewer than two factions are left, so the alliance ended | the faction that left last |
| Field | Type | Description |
|---|---|---|
data.alliance.id | int | ID of the alliance |
data.alliance.name | string | Name of the alliance |
data.faction.id / .name / .tag | int / string / string|nil | The faction the change was about (see above) |
data.factions | list | The member factions after the change, each with id, name and tag. For faction_alliance_dissolved, the one faction that was still in it |
data.reason | string | faction_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.
| Event | When |
|---|---|
bet_opened | A bet was created |
bet_closed | A bet stopped taking entries and waits for its result โ closed by hand or because its deadline passed |
bet_settled | The winning outcome was set and the winners were paid out |
bet_cancelled | A bet was cancelled and every entry refunded |
Event payload (all four):
| Field | Type | Description |
|---|---|---|
data.bookmaker.username | string | Username of the bookmaker |
data.bookmaker.identifier | string | Steam/DayZ ID of the bookmaker |
data.bet_id | int | Internal bet ID, for bookmaker.get(data.bet_id) |
data.bet_code | string | Four-character bet code โ what players pass to /bet <code> |
data.title | string | Bet title |
Additional fields:
| Event | Field | Type | Description |
|---|---|---|---|
bet_opened | data.description | string|nil | Bet description |
bet_opened | data.outcomes | table | List of { id, label } |
bet_opened | data.deadline | string|nil | Entry deadline (YYYY-MM-DD HH:MM:SS, server time) |
bet_opened | data.min_bet / data.max_bet | int|nil | Stake limits, nil = server default |
bet_closed | data.reason | string | "manual" or "deadline" |
bet_closed | data.total_pot | int | Sum of all stakes |
bet_closed | data.entry_count | int | Number of entries |
bet_settled | data.winning_outcome.id / .label | int / string | The winning outcome |
bet_settled | data.total_pot | int | Sum of all stakes |
bet_settled | data.winner_count | int | Players who were paid out |
bet_settled | data.total_payout | int | Sum paid out to the winners |
bet_cancelled | data.refunded_total | int | Sum refunded |
bet_cancelled | data.entry_count | int | Number 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:
| Field | Type | Description |
|---|---|---|
data.mention | string | Mention text (<@id>) โ put it into a message to ping the new member |
data.discord_id | string | Discord user ID |
data.discord_username | string | Discord username |
data.discord_name | string | Display name, or the username if none is set |
data.discord_created_at | string|nil | When the Discord account was created (ISO 8601) |
data.username | string|nil | In-game username on this server โ nil until the member links a game account |
data.rejoined | bool | true 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:
| Field | Type | Description |
|---|---|---|
data.player.username | string | The warned player |
data.noteId | number | The warning note |
data.reason | string | What the warning is for |
data.warnedBy | string | Username of the staff member โ empty for an automatic warning |
data.automatic | bool | true when a restriction violation gave the warning |
data.restrictionId | number|nil | The restriction, for an automatic warning |
data.activeWarnings | number | The player's warnings on this server that have not expired, this one included |
data.expiresAt | string|nil | When 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
);