🪄 Pixie Docs
Overview Commands Blog Status Invite
Setup & Server

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

DirectiveContent
{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}.

GroupExamples
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

Manage Guild + Manage Messages
$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.

CommandWhat 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 resetStops 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

Manage Guild + Manage Messages
$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.

Manage Guild + Manage Messages
$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.

Manage Roles
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.

ContextExtra 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 Rewardsno 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.