Traosyst features a highly advanced, Assyst-inspired tag engine. Tags allow users to create custom commands with complex logic, math operations, and even sandboxed JavaScript execution.
Tags are strictly server-specific. A tag created in Server A will not leak into Server B.
The base command for all tag operations is .tag (or the alias .t).
.tag create <name> <content>β Creates a new tag. (30-second cooldown per user)..tag edit <name> <content>β Modifies an existing tag..tag delete <name>β Deletes a tag..tag rename <old_name> <new_name>β Renames a tag..tag info <name>β Displays the author, usage count, and creation/edit dates..tag raw <name>β Sends the raw code of a tag so you can copy it..tag list [page]β Shows all tags in the current server..tag run <name> [args...]β Executes a tag..tag nukeβ [Bot Owner Only] Wipes all tags across all servers.
You don't actually need to type .tag run mytag. You can just type .mytag. If a tag exists with that name (and it doesn't conflict with a native bot command), it will run automatically.
Tags are built using "subtags" wrapped in {curly_braces}. Subtags can take arguments separated by a pipe |. They can also be infinitely nested inside each other up to 15 levels deep.
If you ever need to type a literal { without triggering the parser, escape it with a backslash: \{.
{args}β The raw string of all arguments passed to the tag.{argslen}β The total number of arguments provided.{arg:index}β Gets a specific argument by index (0-based). Throws an error if missing.{tryarg:index}β Gets a specific argument by index, but returns blank if missing.{set:key|value}β Saves a value to a temporary variable.{get:key}β Retrieves a saved variable.{delete:key}β Deletes a saved variable.
{if:a|operator|b|then|else}β Evaluates a condition. If true, runs thethenbranch; otherwise, runs theelsebranch.- Supported operators:
=(exact match),~(case-insensitive match),>,>=,<,<=. - Note: Unused branches are entirely ignored to save processing power.
- Supported operators:
{choose:option1|option2|...}β Picks a random item from the list.{tag:name|args...}β Executes another existing tag and embeds its output.
{lower:text}β Converts text to lowercase.{upper:text}β Converts text to uppercase.{length:text}β Returns the character count of the text.{replace:find|replaceWith|text}β Replaces all occurrences of a string.{repeat:count|text}β Repeats the text count times.{reverse:text}β Reverses the text.
{range:min|max}β Returns a random whole number between min and max.{max:a|b|c...}β Returns the highest number.{min:a|b|c...}β Returns the lowest number.{abs:n},{sqrt:n},{cos:n},{sin:n},{tan:n}β Standard math functions.{e},{pi}β Math constants.
{userid}β The ID of the user running the tag.{channelid}β The ID of the channel the tag is running in.{usertag:[id]}β Gets a user'sName#1234. If ID is omitted, gets the tag author's name.{avatar:[id]}β Gets a user's avatar URL.{mention:id}β Turns a raw ID into an active <@mention>.{idof:@mention}β Extracts the raw ID from a mention.{lastattachment}β Scans the last 20 messages in the channel and returns the URL of the most recent image/file attachment.
{download:url}β Makes a GET request to the URL and returns the raw text response.- Limits: Max 50 KB payload. Max 5 requests per tag. Private/local IPs are blocked.
For complex logic that native subtags can't handle, you can write raw JavaScript using {js: ...code...}.
The code runs in an ultra-secure, heavily restricted Node.js Virtual Machine.
- It is Sandboxed: You have no access to
process,require,fs,console, or anything related to the host computer. It is impossible to steal.envtokens or break the bot. - Top-Level Await: The code is wrapped in an async function automatically, so you can use
awaitnatively. - Returning Values: The tag will output whatever you
returnat the end of your script. - Image Generation: If your
{js}block returns a rawBufferorUint8Array, the bot will automatically upload it as a PNG attachment rather than text! - Timeouts: The VM will forcefully kill your script if it takes longer than 2.5 seconds to complete.
When your JavaScript runs, the bot injects a few safe, read-only variables into the environment for you to use:
args(Array) β An array of strings containing the arguments passed to the tag. Example: If run with.tagname hello world,args[0]is"hello"andargs[1]is"world".authorId(String) β The Discord Snowflake ID of the user who executed the tag.channelId(String) β The Discord Snowflake ID of the channel where the tag was run.guildId(String) β The Discord Snowflake ID of the server.modalFields(Object|null) β A dictionary of values if the tag was triggered by a Modal Submission. Otherwisenull.
fetch(url, options)β A safe wrapper around the standard Fetch API. You can use it to make external API requests.- Security note: As a measure to prevent SSRF attacks, all requests to
localhost,127.0.0.1,0.0.0.0, and private192.168.x.x/10.x.x.xnetworks are hard-blocked.
- Security note: As a measure to prevent SSRF attacks, all requests to
console.log(...)(and.error,.warn,.info) β Prints data directly to the bot's Discord output stream. Useful for debugging without breaking yourreturnstatement.
{js:
// Fetch an API and return the title
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const data = await res.json();
return "The task is: " + data.title;
}The Tag Engine has been upgraded! Your JavaScript subtags ({js:...}) can now return Discord Embeds and Interactive Buttons, transforming your tags into mini-applications!
To do this, simply return a plain JavaScript object containing an embeds or components array. The engine detects it automatically β no special type field required.
return {
content: "Optional text above the embed",
embeds: [
{
title: "My Embed",
description: "This is a rich embed!",
color: 0x5865F2
}
],
components: [
{
type: 1, // ActionRow
components: [
{
type: 2, // Button
style: 1, // 1=Blurple, 2=Grey, 3=Green, 4=Red
label: "Click Me!",
custom_id: "tagbtn_mytagname_myaction"
}
]
}
]
};When a user clicks a button, the bot needs to know what to do.
- Your button's
custom_idMUST start withtagbtn_. - The second part must be your tag's exact name. (e.g.
tagbtn_testtag_xyz). - When clicked, the bot will re-run your tag from the beginning!
- Any text after the tag name is passed into
args[0],args[1], etc.
Here is a complete example of a tag named clicker that counts how many times a button is clicked.
Command: .tag create clicker {js:...}
{js:
// We use args[0] to track our state!
// If the user just typed .clicker, args[0] is empty, so clicks = 0.
// If they clicked the button, the custom_id was "tagbtn_clicker_1", so args[0] is "1".
let clicks = parseInt(args[0]) || 0;
// If they clicked the button, increase the count!
if (args[0] !== undefined) {
clicks += 1;
}
// Create the new button custom_id for the NEXT click
const nextAction = "tagbtn_clicker_" + clicks;
// Just return an object with embeds/components β no special type field needed!
return {
embeds: [{
title: "Button Clicker",
description: "You have clicked the button " + clicks + " times!",
color: 0x2ECC71
}],
components: [{
type: 1,
components: [{
type: 2,
style: 3,
label: "Click +1",
custom_id: nextAction
}]
}]
};
}Here are some powerful ways you can combine the JS engine with Discord's features.
Because the only way to persist data between button clicks is through the custom_id, you can serialize simple JSON data into the button itself, or use args as a state machine.
{js:
// State is passed via args!
// e.g. tagbtn_mytag_menu_page2
const view = args[0] || 'main';
const data = args[1] || 'default';
if (view === 'main') {
return {
content: "Main Menu",
components: [{ type: 1, components: [{ type: 2, style: 1, label: "Settings", custom_id: "tagbtn_mytag_settings_none" }] }]
};
} else if (view === 'settings') {
return {
content: "Settings Menu",
components: [{ type: 1, components: [{ type: 2, style: 2, label: "Back", custom_id: "tagbtn_mytag_main_none" }] }]
};
}
}By default, anyone can click a button on a tag message. If you want to restrict a button so only the original author can click it, pass the authorId into the custom_id and verify it!
{js:
// When creating the first message:
if (!args[0]) {
return {
content: "Only you can click this button!",
components: [{ type: 1, components: [{ type: 2, style: 1, label: "Click Me", custom_id: `tagbtn_mytag_click_${authorId}` }] }]
};
}
// When the button is clicked:
const originalAuthor = args[1];
if (authorId !== originalAuthor) {
// If someone else clicks it, return an ephemeral error (handled by bot automatically) or a polite message:
return "β You are not allowed to use this button.";
}
return "β
Verification passed!";
}To prevent the bot from crashing or being abused, the parser enforces the following hard limits per execution:
- 15 maximum nesting levels deep.
- 500 total parser iterations.
- 100 variables maximum via
{set}. - 5 total HTTP requests (combined
{download}andfetch()). - 10,000 characters maximum total output length.
- Legacy mode (no
IS_COMPONENTS_V2flag): 1 embed, 1 action row, 5 buttons max. - Components V2 mode (
flags: 1 << 15): Up to 20 top-level components (Discord's real limit is 40, but the bot enforces 20 to prevent abuse). Max 4,000 chars across all text.
Building advanced tags can sometimes lead to unexpected errors. Here's a troubleshooting guide:
This means the bot took longer than 3 seconds to respond to Discord, or sent a payload Discord completely rejected.
- Cause 1: Your
{js}script is doing something too slow (like multiple heavyfetchcalls). The script must finish in under 2.5 seconds. - Cause 2: You returned
{ content: "" }(an empty string). Discord rejects this in V2 mode. Just omitcontententirely if you don't need text. - Cause 3: You didn't prefix your button's
custom_idwithtagbtn_tagname_.
This means your JavaScript code threw an exception. The bot will reply to you ephemerally with the exact error.
- Double check your variable names.
- Ensure APIs you are calling actually exist and return valid JSON (use
try/catchblock aroundres.json()).
If you uploaded a .js or .txt file containing your tag code to bypass Discord's character limit, but the bot says "You must provide tag content":
- Ensure the file is actually named
.jsor.txt. - Make sure you still provided the tag name! The correct syntax is
.tag create mytagname+ [Attached File].
Remember that select menus append their values to the END of the args array. If your custom ID is tagbtn_mytag_menu1, then args[0] will be "menu1" and args[1] will be the value the user selected from the dropdown!
Components V2 is a powerful Discord feature that lets you build fully structured, layout-driven messages using a flat component tree instead of embeds.
To activate it, add flags: 1 << 15 (= 32768) to your return object. When this flag is set:
contentandembedsfields are ignored by Discord- All text must go in Text Display components (type 10)
- You can use layout components like Containers, Sections, Separators, and Media Galleries
- Up to 20 top-level components are allowed by the bot (Discord allows 40)
- Max 4,000 characters across all text combined
| Type | Name | Description |
|---|---|---|
| 1 | Action Row | Container for buttons or a select menu |
| 2 | Button | Clickable button (styles: 1=Blue, 2=Grey, 3=Green, 4=Red, 5=Link) |
| 3 | String Select | Dropdown with custom text options |
| 5 | User Select | Dropdown that lists server members |
| 6 | Role Select | Dropdown that lists server roles |
| 7 | Mentionable Select | Dropdown of users and roles combined |
| 8 | Channel Select | Dropdown that lists channels |
| 9 | Section | Side-by-side text + a thumbnail or button accessory |
| 10 | Text Display | Markdown text block (replaces content) |
| 11 | Thumbnail | Small image used as a Section accessory |
| 12 | Media Gallery | Grid of 1β10 images with optional captions |
| 13 | File | Displays an uploaded attachment |
| 14 | Separator | Visual divider with padding |
| 17 | Container | Visually grouped card with optional color bar |
When a user picks an option from a select menu, the selected values are passed as additional args after any existing args from the custom_id.
For example, if your custom_id is tagbtn_mymenu_page2 and the user selects option_a, your tag receives:
args[0]="page2"args[1]="option_a"(the selected value)
If a tag is triggered by a component interaction (like a button click), it can pop up a Modal instead of updating the message. To do this, return a modal object instead of components or embeds.
return {
modal: {
title: "Feedback Form",
custom_id: "tagbtn_mytag_submitfeedback",
components: [
{
type: 1, // Action Row
components: [{
type: 4, // Text Input
custom_id: "feedback_text",
label: "Your Feedback",
style: 2, // Paragraph
required: true
}]
}
]
}
};When the user submits the modal, the bot re-runs your tag using the custom_id you provided. The values they typed will be available in the modalFields object!
// Inside your tag code when 'submitfeedback' triggers:
const feedback = modalFields["feedback_text"];
// Now you can return a normal components v2 payload thanking them!This tag demonstrates every major V2 component type. Create it with .tag create showcase {js:...}:
{js:
let page = args[0] || 'home';
if (page === 'home') {
return {
flags: 1 << 15,
components: [
{
type: 17, // Container
accent_color: 0x5865F2,
components: [
{ type: 10, content: "# π Components V2 Showcase\nThis message is built entirely with Components V2." },
{ type: 14, divider: true, spacing: 1 }, // Separator
{
type: 9, // Section
components: [
{ type: 10, content: "## π What is this?\nA full showcase of every **Components V2** layout and interactive element, built inside a single tag." },
{ type: 10, content: "-# Click the buttons below to explore!" }
],
accessory: {
type: 11, // Thumbnail
media: { url: "https://cdn.discordapp.com/embed/avatars/0.png" },
description: "Bot icon"
}
},
{ type: 14, divider: true, spacing: 1 }, // Separator
{
type: 12, // Media Gallery
items: [
{ media: { url: "https://picsum.photos/seed/v2a/400/200" }, description: "Gallery image 1" },
{ media: { url: "https://picsum.photos/seed/v2b/400/200" }, description: "Gallery image 2" }
]
},
{ type: 14, divider: false, spacing: 2 }, // Spacer
{ type: 10, content: "### π Navigate" },
{
type: 1, // Action Row - Buttons
components: [
{ type: 2, style: 1, label: "Info", emoji: { name: "βΉοΈ" }, custom_id: "tagbtn_showcase_info" },
{ type: 2, style: 3, label: "Selects Demo", emoji: { name: "π½" }, custom_id: "tagbtn_showcase_selects" },
{ type: 2, style: 2, label: "Modals Demo", emoji: { name: "π" }, custom_id: "tagbtn_showcase_modaldemo" },
{ type: 2, style: 4, label: "Close", emoji: { name: "β" }, custom_id: "tagbtn_showcase_close" }
]
}
]
}
]
};
} else if (page === 'info') {
return {
flags: 1 << 15,
components: [{
type: 17,
accent_color: 0x2ECC71,
components: [
{ type: 10, content: "## βΉοΈ Info Page\nHere is some information about this bot. This page was loaded by clicking a button β no new message was posted!" },
{ type: 14, divider: true, spacing: 1 },
{ type: 10, content: "- Built with **Node.js** and **Discord.js v14**\n- Runs on **AWS EC2** inside **Docker**\n- Tag engine supports `{js}`, `{if}`, `{set}`, `{get}`, and more!" },
{
type: 1,
components: [
{ type: 2, style: 2, label: "β Back", custom_id: "tagbtn_showcase_home" }
]
}
]
}]
};
} else if (page === 'selects') {
return {
flags: 1 << 15,
components: [{
type: 17,
accent_color: 0xF39C12,
components: [
{ type: 10, content: "## π½ Select Menus Demo\nPick an option below! The bot will re-run the tag with your selection passed as an argument." },
{ type: 14, divider: true, spacing: 1 },
{
type: 1, // Action Row with String Select
components: [{
type: 3, // String Select
custom_id: "tagbtn_showcase_picked",
placeholder: "Choose a colour...",
options: [
{ label: "Red", value: "red", emoji: { name: "π΄" } },
{ label: "Green", value: "green", emoji: { name: "π’" } },
{ label: "Blue", value: "blue", emoji: { name: "π΅" } }
]
}]
},
{
type: 1,
components: [{ type: 2, style: 2, label: "β Back", custom_id: "tagbtn_showcase_home" }]
}
]
}]
};
} else if (page === 'picked') {
const picked = args[1] || 'nothing';
const colours = { red: 0xE74C3C, green: 0x2ECC71, blue: 0x3498DB };
return {
flags: 1 << 15,
components: [{
type: 17,
accent_color: colours[picked] || 0x95A5A6,
components: [
{ type: 10, content: `## You picked: **${picked}** β
\nThe container's accent bar changed colour to match your choice!` },
{
type: 1,
components: [
{ type: 2, style: 2, label: "β Try again", custom_id: "tagbtn_showcase_selects" },
{ type: 2, style: 2, label: "β Home", custom_id: "tagbtn_showcase_home" }
]
}
]
}]
};
} else if (page === 'modaldemo') {
// Return a modal popup! No 'flags' needed for modals.
return {
modal: {
title: "Tell us about yourself",
custom_id: "tagbtn_showcase_modalsubmit",
components: [
{
type: 1,
components: [{
type: 4, // Text Input
custom_id: "input_name",
label: "What's your name?",
style: 1, // Short
required: true
}]
},
{
type: 1,
components: [{
type: 4,
custom_id: "input_color",
label: "Favorite color?",
style: 1,
required: false
}]
}
]
}
};
} else if (page === 'modalsubmit') {
// Read from the modalFields object!
const name = modalFields["input_name"] || "Mystery Person";
const color = modalFields["input_color"] || "Invisible";
return {
flags: 1 << 15,
components: [{
type: 17,
accent_color: 0x9B59B6,
components: [
{ type: 10, content: `## π Modal Submitted!\nHello, **${name}**! Your favorite color is **${color}**.` },
{
type: 1,
components: [
{ type: 2, style: 2, label: "β Home", custom_id: "tagbtn_showcase_home" }
]
}
]
}]
};
} else if (page === 'close') {
return {
flags: 1 << 15,
components: [{
type: 17,
accent_color: 0x95A5A6,
components: [
{ type: 10, content: "π **Showcase closed.**\nType `.showcase` to open it again." }
]
}]
};
}
}