Custom embed codes
Write one embed code (tags, variables and buttons) and reuse it in welcome messages, tickets, level-ups and everywhere else Righteous asks for one.
Righteous asks for an "embed code" in a lot of places: server messages, ticket greetings, level-up announcements, sticky messages, trap DMs and your now-playing card. They all speak the same small language. Those guides cover when each message fires; this one covers what you type into it.
The language has no permission gate of its own: each surface carries one, usually Manage
Server. ,customembed (,ce), the
scratchpad for trying a code out before you save it, needs Manage Messages.
Post a code and see what you get
A code is plain text with {tag: value} chunks in it, and anything that is not a recognised tag
stays plain text. So the simplest possible code is a sentence:
Syntax
,customembed <code>
Example
,customembed hey everyone
Add a tag the parser knows and the same command posts an embed. Parts are separated by $p, and
their order does not matter. Each tag is found by name, never by position:
Syntax
,customembed <code>
Example
,customembed {title: Announcement}$p{description: Doors open at 8.}$p{color: #fb4868}Variables go inside a tag, wrapped in their own braces, and fill in when the message is sent:
Syntax
,customembed <code>
Example
,customembed {title: Welcome}$p{description: {user.mention} is member number {guild.membercount} of {guild.name}.}$p{thumbnail: {user.avatar}}The embed builder is the same parser in a browser with a live preview, and nothing
there is saved or sent. It's the cheap place to get a code wrong. So is
,copyembed <message link>: it reads an existing
message back out as the code that would rebuild it, which beats starting from a blank line. You can
skip hunting for the link: right-click the message, then Apps → Copy Embed Code, for the same
result. (Either way, a message built with Components V2 is refused rather than
half-converted — its layout can't be turned back into embed code.)
Build the embed out of tags
| Tag | What it sets | Takes |
|---|---|---|
{title: text} | the heading | text |
{description: text} | the body | text |
{color: value} | the stripe down the left | a hex colour, or random |
{url: link} | makes the title clickable | an http(s) link |
{thumbnail: link} | the small image, top right | an image link |
{image: link} | the large image, at the bottom | an image link |
{author: name && icon && link} | the small line above the title | name required, the other two optional |
{footer: text && icon} | the small line at the bottom | text required, icon optional |
{field: name && value} | a labelled block: add && inline to sit it beside the next one | both halves required |
{timestamp:} | a time on the footer | nothing; the tag alone is the signal |
{content: text} | text outside the embed, in the message body | text, up to 2,000 characters |
{button: ...} | a button under the message | see below |
Tags with several parts split on &&, so an author name, a footer or a button label cannot contain
a literal &&. {field:} is the exception: its name is the first part and the whole remainder is
the value, && and all, so {field: A && B && C} gives a field called A whose value is B && C.
Loose text in no tag does the same job as {content:}, and several {content:} tags accumulate in
code order where a second {title:} is ignored. A chunk that looks like a tag but is not one
({banner: x}, a typo) is dropped silently rather than printed, which is why a misspelling gives you
nothing at all rather than an error.
Fill in names and numbers with variables
A variable is a token in braces that resolves when the message is sent. Which ones resolve depends on where the code runs: a welcome message knows the member who just joined, a level-up message knows the level:
| Family | Fills in |
|---|---|
{user}, {user.mention}, {user.name}, {user.avatar}, {user.joined} … | anywhere a code renders: the member the message is about |
{guild.name}, {guild.membercount}, {guild.icon}, {channel.name} … | anywhere a code renders |
{color.random} | anywhere: a fresh hex colour on every render |
{ticket.id}, {ticket.opener}, {ticket.reason}, {ticket.answer1} … | only in the ticket greeting: the one message rendered while a ticket exists |
{level}, {xp.into}, {rank}, {reward} … | only in a level-up message set with ,levels embeds |
{track.name}, {album.cover}, {artist.plays}, {spotify.url} … | wherever the member it renders for has a Last.fm account linked |
Out of its context a variable renders as its own name, not as nothing. The ticket and level
tokens are filled in by a pass that runs only on the surface that owns them; anywhere else they
reach the ordinary parser, which has never heard of them and does what it does with any unknown
token: drops the braces, keeps the text. So {ticket.id} on the ticket panel renders the bare
words ticket.id, which is the usual surprise, since the panel exists before any ticket does. A
plain misspelling looks identical: {usr} renders as usr. A bare word in your embed is either a
typo or a token asking a surface for something it has no idea about.
The tokens that really do resolve to nothing are the music ones, for a member with no linked account or no scrobble to read.
Every token, searchable, is on the variables page;
,customembed variables prints it in
Discord.
Add buttons
Syntax
,customembed <code>
Example
,customembed {description: Read these first.}$p{button: Rules && https://example.com/rules}| Part | Means | Example |
|---|---|---|
| label | the text on the button | Rules |
| link | where it sends people | https://example.com |
| emoji | unicode, or one of your server's, written <:name:id> | 🎧 |
| colour | link, blue, green, grey or red | green |
disabled | greys it out | disabled |
Parts are matched by kind rather than position, so a label, a link and an emoji can appear in any order. A label or an emoji is required and the rest is optional; you get up to 25 buttons, five to a row, and one the parser cannot build is dropped while the rest of the message sends.
Only link buttons do anything when clicked. The coloured ones are decoration: the bot acknowledges the click and does nothing. Nor can a button carry an arbitrary image; Discord has no such thing, and a custom emote is what that idea becomes.
Delete the message after a while
++delete is not a tag. A tag describes what the message is; an action tells the bot what to do
with it, never renders, and goes at the very end of the code:
Syntax
,customembed <code> ++delete <seconds>
Example
,customembed {description: Back in five.} ++delete 30One flag, three parsers: the surface you write it on decides what it will take.
| Where you write it | Takes | Default |
|---|---|---|
A code rendered as it is sent: ,customembed, ,webhooks send, a ticket greeting | a whole number of seconds, 1 to 600 | off |
A saved server message: ,welcomemessage add, and its goodbye, boost and unboost twins | a whole number of seconds, 1 to 60 | off |
A saved level-up message: ,levels embeds | a duration with a unit, up to 1h (30s, 5m) or off to clear it | off |
The differences bite. ++delete 30 is thirty seconds on the first two rows and an error on the
third, which wants 30s. ++delete 1h is an hour on the third row and refused outright on the
first, which answers "Auto-delete must be a whole number, like 3."
Rows one and three refuse a value they cannot read rather than rounding it to something
convenient: deleting somebody's message at a time they never asked for is worse than not deleting
it. The saved server messages coerce instead, and that makes the second row the one to be
careful on: it checks your text against the 1-to-60 bounds before turning it into a number, so
anything with a letter in it satisfies neither bound and slips through, then gets truncated to
whatever digits it happens to start with. ++delete 1h saves as 1 second. ++delete 6O, typed
with a letter O, saves as 6 seconds. Neither says a word.
Where the flag may sit differs too, and that one costs you text. ,customembed and ,levels embeds
look at the very end of the code and nowhere else, so a ++delete in the middle is left alone:
,customembed renders it as ordinary text, and ,levels embeds refuses the save and asks you to
put the flag at the end with a value. The four server-message commands look for the word anywhere
and cut the code off where they find it, so a mid-code ++delete silently truncates everything
after it: ,welcomemessage add #general {description: Hi} ++delete 5 $p{footer: bye} saves a
welcome message with no footer and tells you it worked. Put the flag last, always.
Four surfaces ignore the timer. Join DMs and trap DMs do, because a DM the member may not have
opened should not vanish with no trace; ,pages and
,editembed do because they edit a message that
already exists rather than sending one, and a timer would kill a paginator mid-read.
The flag is still stripped there, so it never leaks into the message as visible text. On welcome, goodbye, boost, unboost and level-up messages it is read when you save the code and kept as that message's own auto-delete setting. See server messages.
Switch the whole message to Components V2
Adding {v2:} anywhere renders the code as a Components V2 container instead of a classic embed.
Same $p syntax, same variables, same buttons, plus three tags with no classic equivalent:
Syntax
,customembed <code>
Example
,customembed {v2:}$p{title: New here?}$p{gallery: https://example.com/a.png && https://example.com/b.png}$p{separator:}$p{section: Start in the rules channel && https://example.com/icon.png}| Tag | What it adds |
|---|---|
{gallery: link && link …} | up to ten images in one grid; an {image:} counts as the first of them |
{section: text && accessory} | text beside a thumbnail (one image link) or a button, never both |
{separator:} | a divider line |
The syntax reads more general than it is, so know three limits first: only the first
{section:} renders, {separator:} renders once however many you write, and the layout order
is fixed (text, gallery, section, separator, buttons) wherever the tags sit. It is a template, not
a layout language.
Components V2 also cannot show an author or footer icon or put fields into inline columns; those
are dropped with a warning rather than refusing the save. Its text budget is 4,000 characters where
a classic embed's is 6,000, so a code that fits as an embed can be too big once {v2:} is added.
,webhooks send refuses a {v2:} code outright
and says why: a guild webhook can only send classic embeds and buttons, so refusing beats posting
half a message.
,pages is the one to watch, because it refuses much
less than you would expect. The only code it turns down is one that would leave a page set with
Components V2 on some pages and classic embeds on others: Discord fixes a message as one or the
other for life, so a set that mixed them could not page through itself. A set is built from a
message that already carries a classic embed, which means a {v2:} page saves cleanly into a
single-page set, reports success, and then never renders: the paginator finds a container where
its message is a classic embed and declines the edit rather than throw, so the message keeps showing
what it showed before. Keep page sets classic and use ,customembed where you want a container.
Know where a code works
| Where | Set with |
|---|---|
| Welcome, goodbye and boost messages, and join DMs | Server messages |
| Punishment confirmations (Server Premium) | ,custommessage |
| Ticket greetings, and the ticket panel | ,ticket message, ,ticket setup |
| Level-up announcements | ,levels embeds |
| Sticky messages (Server Premium) | ,stickymessage add |
| Trap channel DMs | ,trap dm, Trap channel |
| A one-off webhook message | ,webhooks send |
| Your now-playing card (Self Premium) | ,lastfm mode |
| A one-off post, or editing one you already sent | ,customembed, ,editembed |
Autoresponders do not take embed codes
,autoresponder replies go through the
variable pass only. They never reach the embed parser. {user} and {guild.name} fill in as
normal, but {title: Hi} posts as the literal text title: Hi, $p prints as itself, and there
is no embed and no button. Music tokens do not resolve there either. Keep autoresponder replies to
plain text and variables: ++delete is the one flag they do take, because the autoresponder
parses it itself rather than getting it from the embed parser.
Common issues
Nothing posted at all. A code whose every tag is misspelled parses to an empty message, which
Discord will not accept, so nothing is sent, quietly. Add a {description:} and see if that posts.
A tag printed as visible text. You are on a surface that does not parse codes: the autoresponder
is the one that catches people. Where codes are parsed an unrecognised tag is dropped, never
printed, so title: Hi in a channel means the whole string was treated as text.
A word from my code shows up bare in the embed. A token that lost its braces and kept its text
is either a misspelling ({usr} becomes usr) or a token on a surface that cannot fill it in: a
{ticket.*} on the ticket panel, a {level} outside a level-up message. Check the spelling on the
variables page, then check you are on the surface that owns the token. A
variable that came out empty is a different fault and almost always a music one: no linked
account, or nothing scrobbled yet.
A line starting with a word and a colon vanished. Notice: read this is shaped like a mistyped
tag, so it goes with the rest of them. Wrap it: {content: Notice: read this}. An unreadable colour
is the same class of quiet failure: the embed falls back to black rather than not posting at all,
so use a hex value like #fb4868, or random.
A {v2:} page saved, but the paginator kept showing the old page. A paginated message is
created as a classic embed and Discord will not convert one into a Components V2 container, so the
page is stored and then quietly refused at render time. Put that page back to classic tags.
A {v2:} code posted as an ordinary embed. The Components V2 render hit a cap, so the bot fell
back to a classic embed rather than posting nothing, and the gallery, section and separator went
with it. Where you save a code, the bot refuses instead and names the cap it busted.
Nothing here fixed it. The shared common issues page works the same ground from the symptom end.