Embed Scripting
A small scripting language for building embeds and buttons out of one string - live variables (member, server, channel, date/time), conditionals, and buttons, all resolved fresh every time the message is posted or re-rendered.
This is for people who want full control over an embed's exact layout and content in one go -
for a click-through builder instead, see Embed Builder. The two are
independent; a script-created embed isn't editable through $embed and vice versa.
Don't want to hand-write the syntax below at all? The visual Embed Builder lets you build a script by filling out a real form with a live Discord-accurate preview, then copies out the exact command ready to paste - pick which system it's for (welcome, boost shoutouts, level-ups, etc.) and its variables switch to match. It also imports an existing script back into the form if you want to keep editing one visually.
The basics
A script is one string made of directives separated by $v. Each
directive looks like {name: content} (a few, like {timestamp}, take no
content). Start the whole thing with {embed}$v to build a real embed instead of a
plain text message:
{embed}$v{title: Server Rules}$v{description: Be nice to each other.}$v{color: #ff66aa}
Leave off the leading {embed}$v and use just {message: ...} to send
plain text instead of an embed.
Directives
| Directive | Content |
|---|---|
{title: text} | Embed title |
{description: text} | Embed body |
{color: #hex} | A hex color like #ff66aa, or {user.pfp_color} to auto-match the dominant color of their avatar. Invalid/missing falls back to the server's default embed color |
{image: url} | Large image at the bottom of the embed |
{thumbnail: url} | Small image in the top-right corner |
{url: url} | Makes the title a clickable link |
{timestamp} | Adds the current time to the embed's footer area (no content - just the bare directive) |
{author: name && icon_url && url} | Only name is required - leave the rest blank to skip them, e.g. {author: {user}} |
{footer: text && icon_url} | icon_url is optional |
{field: name && value && inline} | Repeat this directive for multiple fields. End the value with a literal word inline to make it inline, e.g. {field: Rank && {user.top_role} inline} |
{button: style && label && url && disabled && emoji} | See Buttons below - repeat for multiple buttons |
{message: text} | Plain text sent alongside (or instead of) the embed |
The four-part directives (author, footer, field,
button) split their content on && - trailing parts can just be
left out entirely if you don't need them.
Variables
Drop a {variable.name} anywhere inside a directive's content and it's replaced with
the live value when the script renders. A bare {user} is shorthand for
{user.display_name}.
| Group | Examples |
|---|---|
| user | {user}, {user.mention}, {user.id}, {user.tag}, {user.avatar}, {user.joined_at}, {user.created_at}, {user.top_role}, {user.role_list}, {user.boost}, {user.badges}, {user.join_position}, {user.join_position_suffix}, {user.pfp_color} |
| guild | {guild.name}, {guild.id}, {guild.count}, {guild.owner_id}, {guild.icon}, {guild.boost_count}, {guild.boost_tier}, {guild.channels_count}, {guild.text_channels}, {guild.role_count}, {guild.vanity} |
| channel | {channel.name}, {channel.id}, {channel.mention}, {channel.topic}, {channel.category_name}, {channel.slowmode_delay} |
| date/time | {date.now}, {date.now_proper}, {date.utc_timestamp}, {time.now}, {time.now_military} |
Who {user.*} resolves to depends on context: on createembed/scripttest
it's whoever ran the command; on a paginated page turn it's whoever clicked the reaction; on a
manual pagination update/set/remove re-render it falls back to the message's original
author.
Wrap a variable in proper() or lower() to transform it -
{proper(user.name)} title-cases it, {lower(guild.name)} lowercases it.
An unknown or misspelled variable is left as literal text (e.g. {user.typo} shows up
exactly like that) rather than vanishing, so mistakes are easy to spot.
A date/time variable like {user.joined_at} renders two different ways depending on
where you put it. In a description or field value, it shows as
Discord's live, auto-formatted timestamp. In a title, author name,
footer, field name, or button label - none of
which Discord lets render that live tag at all - it's converted to a plain readable date/time
string instead, so it always looks right no matter which directive it ends up in.
Conditionals
{if COND}...{elseif COND}...{else}...{/if} - no nesting one inside another, but as
many separate if-blocks in the same script as you want. A condition is a variable compared with
==/!=, or just the variable by itself as a truthy check (empty text,
0, false, none, and no all count as false).
{embed}$v{if {user.boost} == Yes}{description: Thanks for boosting, {user}!}{else}{description: Welcome, {user}!}{/if}
Buttons
{button: style && label && url && disabled && emoji} adds up to 5 buttons per row
(Discord's own limit). style is one of link, blurple,
green, grey/gray, or red. Only link
buttons do anything on their own (they just open the URL) - a non-link button posted this way has no
built-in action; click it and Pixie tells you so. To actually make one do something, attach
a role to it with buttonrole add afterward. A link button
with no real URL (left blank, or a variable that resolves empty) is dropped rather than breaking the
whole message - scripttest/createembed tell you when that happens.
{embed}$v{title: Links}$v{button: link && Support Server && https://discord.gg/example}
The optional 5th field is an emoji - either a plain one (🥷) or a real custom server
emoji in <:name:id> form. Combine with disabled (leaving
url blank) for a decorative, non-clickable badge:
{button: grey && {guild.boost_count} && && disabled && <:boost_emoji:1534009361058500749>}
Multiple embeds in one message
Separate whole scripts with --add to post up to 10 embeds in a single message
(Discord's own cap). Only the first segment's {message: ...} text is used;
buttons from every segment are combined onto the same message.
{embed}$v{title: Page One} --add {embed}$v{title: Page Two}
Testing and posting
$scripttest <script>
$createembed <script>
Also $testscript/$scriptembed. scripttest just shows you the
result - nothing else happens, and it isn't trackable for pagination. createembed posts
it as a real message and starts tracking it, which is what pagination and
buttonrole both require. If a script produces nothing at all (no text, embed, or
buttons) or Discord rejects it (e.g. a field over its character limit), you'll get a specific error
back instead of a blank success. A typo that isn't fatal - an invalid hex color, an unrecognized
button style - still posts (using a sensible default) but adds a follow-up warning telling you
exactly what it ignored.
Pagination
Every command below needs Manage Guild + Manage Messages.
Only messages posted with createembed can have pages added - this mirrors how the
feature this was modeled on works. Navigation is two reactions (⬅️/➡️) on the message itself, not
buttons, and wraps around in both directions. Every page re-renders against live variables
each time it's shown, so a page showing {guild.count} always reflects the current
member count, not whatever it was when the page was written.
| Command | What it does |
|---|---|
pagination add <message> <script> | Appends a new page. Going from 1 page to 2 automatically adds the nav reactions. |
pagination update <message> <page#> <script> | Replaces a page's script in place. |
pagination remove <message> <page#> | Deletes a page (can't remove the last one - use delete for that). |
pagination set <message> <page#> | Jumps the message to a specific page. |
pagination list <message> | Shows the page count and which one is current. |
pagination delete <message> | Stops tracking the message (the message itself isn't deleted) and clears the nav reactions. |
pagination restorereactions <message> | Re-adds the ⬅️/➡️ reactions if they got removed somehow. |
pagination reset | Stops tracking every paginated message in the server at once. |
<message> accepts either a raw message ID or a full message link, everywhere above.
Stickying a message
$stickyembed <message>
Also $sticky. Not a pagination feature - works on any createembed message,
with or without extra pages. Toggles whether it stays pinned to the bottom of its channel,
automatically re-posted whenever something pushes it down. Run the same command again to unstick it.
Stickying joins the same shared sticky system embedbuilder's own "Stickied" toggle uses, so a
stickied createembed message correctly stacks alongside an embedbuilder sticky,
the confessions disclaimer, or a quests auto-leaderboard in the same channel as one ordered block,
instead of each independently fighting to be the "last" message. It re-renders against live
variables on every repost, same as pagination's own pages do.
Server name/icon/banner changes
{guild.name}/{guild.icon}/{guild.banner} update automatically -
the moment you rename the server or change its icon/banner in Server Settings, every tracked
createembed message whose script references one of those three variables gets
re-rendered on the spot. Nothing to run, nothing to remember.
$serverprofileupdated
Also $refreshembeds, $syncembeds, /script refresh. A manual
force-refresh for edge cases the automatic trigger can't see - e.g. you just added a
{guild.icon} reference to an existing script via pagination update, so it's
now stale relative to a profile change from before that edit. Scans every tracked message, re-renders
the ones that reference those three variables, leaves everything else untouched. Safe to run any
time, including when nothing's changed.
Button roles
Attaches a real, working role-toggle button to any message this bot sent - it
doesn't have to be one made with createembed, unlike pagination. Clicking gives the
role if you don't have it, removes it if you do.
buttonrole add <message> <role> <style> <label>
buttonrole remove <message> <role>
buttonrole removeall <message>
buttonrole reset
style is blurple, green, grey/gray,
or red (no link - a role button always needs a click handler, which a
link-style button can't have). buttonrole list <message> (no permission needed)
shows what's attached without changing anything. @everyone and managed roles (bot/booster/integration
roles) can't be attached, neither can a role positioned above Pixie's own highest role, and neither
can a role at or above your own highest role (server owner is exempt from this last check) -
that last one exists so Manage Roles alone can't be used to set up a button that hands out a role you
couldn't normally assign yourself. All four are rejected up front with a clear reason rather than
failing silently when someone clicks.
buttonrole reset only stops Pixie from tracking the buttons - it doesn't remove them
from the messages themselves, so old buttons stay visible but inert until someone clicks one.
Putting it together
A welcome-style embed with a conditional booster shoutout, a field, and a link button:
$createembed {embed}$v{title: Welcome to {guild.name}!}$v{description: Hey {user.mention}, you're member #{guild.count}.}$v{color: #ff66aa}$v{if {user.boost} == Yes}{field: Booster && Thanks for boosting! inline}{/if}$v{footer: Joined {user.joined_at}}$v{button: link && Read the Rules && https://discord.com/channels/...}
Using this for welcome messages, boost shoutouts, vanity rewards, level-ups, jail messages, and VC XP rewards
Welcome/Leave Messages, Booster Shoutouts,
Vanity Rewards, Chat
Leveling's level-up message, Jail's jail-channel message, and
VC XP's reward-role message all still use their own simple
<token>/{token} setup by default - nothing changes for that. But set
the message to a script (start it with {embed}$v) and it takes over completely, exactly
like the rest of this page - full custom embed, colors, fields, buttons, all of it. Switch back to
plain token text any time and it reverts to the simple system.
| Context | Extra variables |
|---|---|
| Welcome/Leave | {inviter.mention}/{inviter} (pings, every time), {inviter.name} (plain, non-pinging - use this instead of {inviter.mention} if you don't want to re-notify the inviter on every new join through their invite, the same reason the old system had a separate <inviter_noping> token), {inviter.is_real} (Yes/No - use in an {if} instead of the old [ifinvitedbyuser] tag), {inviter.invites_count} |
| Booster Shoutouts | {boost.count}, {boost.times_text} ("time"/"times") - Test/Preview show these as a fixed 1/time since there's no real boost event to count yet, not a broken-looking placeholder |
| Vanity Rewards | no extras beyond the normal variables. Legacy mode stays no-ping, same as always - script mode is full control like everywhere else on this page, so whether {user.mention} pings depends on where you put it: inside {message: ...} it pings for real, inside {description: ...} (or any other embed field) it shows but stays silent, same rule Discord itself already applies to embeds |
| Chat Leveling (level-up message) | {level}/{level.num} (the level just reached), {level.reward_role} (mention of the role granted at this level, or "none"), {level.has_reward} (Yes/No). {user.*} is the member who leveled up. Script mode fully replaces the legacy "Earned <role>!" line - use {level.reward_role} yourself if you want it |
| Jail Message | {actor.mention}/{actor} (pings), {actor.name} (plain, non-pinging), {reason} (falls back to "no reason provided"). {user.*} is the member being jailed, same as the legacy {user} token |
| VC XP (reward-role message) | {reward.role} (mention of the role just granted), {reward.role_name} (plain name, non-pinging), {reward.xp} (formatted total, e.g. "600 XP (10h 0m)"). {user.*} is the member who earned the reward - unlike Chat Leveling there's no "level number", just a role and an XP total |
Every one of these also has a plain prefix command to set the message directly (paste text or a whole
script in one go - no need to click through a panel), a preview command that posts a button showing
the render privately, and now a matching test command that sends the real thing end-to-end
in its configured channel: welcomemessage setmsg/preview/test,
leavemessage setmsg/preview/test, boostershoutout setmsg/preview/test,
vanity setmsg/preview, jailconfig setmsg/preview, and
xp setmsg + previewlevelup/testlevelup,
vcxp setmsg + previewreward/testreward. Running setmsg
with no message just shows the current one and how to change it - it never resets anything by
accident; use the word reset as the message to actually reset.
Script mode replaces the feature's own defaults entirely - welcome's avatar thumbnail/banner
image and boost shoutout's badge-count buttons only show up if your script recreates them
({user.display_avatar} as a {thumbnail: ...}, the boost badge row as two
disabled emoji buttons - see Buttons). If a script fails to render or
produces nothing, a plain fallback message still gets sent rather than nothing at all.
Both panels ($welcomemessage/$leavemessage/$boostershoutout)
have a Preview button next to Test - Preview renders your current message exactly
as it would send (script or plain) and shows it to only you, with zero side effects: no real post,
no notifications, nothing. Test does the real thing end-to-end, posting for real in the configured
channel. When the message is script-controlled, the panel also disables the old Image/Send-in-Embed
toggles (the script decides that now) and its footer reminds you how to switch back to the classic
system.