Tutorials
Interactive Messages
Build a custom command with durable buttons and fresh Lua runs.
This page is not available in es yet, so the en version is being shown.
This tutorial turns a plain custom command into a small menu. Each click reloads the
same script in a fresh Lua session and calls the function named by the button. Macchi
stores a route to the function, never a Lua closure or a live session.
Create The Command
Create a personal custom command named guide, then use Edit source and paste:
function rules()
macchi.reply("Read the rules before posting.")
end
function help()
macchi.reply("Ask a moderator if the guide does not answer your question.")
end
function run()
message.new()
:text("What do you need?")
:button("rules", "Rules")
:button("help", "Help")
:send()
end
Run the command and click both buttons. run builds the message. rules and help
are normal Lua entries loaded later for the person who clicks.
Keep The Route Valid
- Function names must match the first argument passed to
:button. - The published version or current draft is pinned into the component route.
- The clicking person, server, channel, and route are validated before Lua runs.
- Message and component objects are inert until
:send()or
macchi.replyMessage(message)is called.
Do not keep state in a global variable between clicks. If the workflow needs durable
values, use the storage API with an explicit capability and scope.
Use The Current Components
Components V2 can organize text without turning every reply into a panel. A reply made
only from a string stays a regular Discord message. Build a message when you need layout:
local gallery = components.gallery()
:item("https://example.com/preview.webp", "Dashboard preview")
message.new()
:title("Release notes")
:display("The new dashboard is ready for review.")
:separator(false, "large")
:addGallery(gallery)
:sectionButton("Open the guided review.", components.button("review", "Review"))
:accent("D95C5C")
:send()
The message builder also supports thumbnail sections, selects, entity selects, a
container spoiler, and link buttons. Galleries accept one to ten direct HTTP(S) media
URLs. Macchi does not download a URL and pretend it is a Discord attachment.
Modals use Discord’s current labeled fields:
local form = components.modal("Publish", "publish")
form:radio("visibility", "Visibility")
:option("Public", "public")
:option("Unlisted", "unlisted")
:done()
:checks("topics", "Topics", "Choose every relevant topic", false, 0, 3)
:option("Lua", "lua")
:option("Gaming", "gaming")
:done()
:check("confirmed", "I reviewed the information")
message.new():text("Ready to publish?"):addModal(form):send()
Use ctx.component:field("visibility") for a single value. For checkbox groups and
uploads, use fieldCount(key) and fieldAt(key, index). Upload fields expose validated
Discord CDN URLs as scalar values; they never expose raw Discord objects to Lua.
Use display(text) for inline modal guidance, and users, roles, channels, or
mentionables when the answer should be a Discord entity instead of free text.
Continue
Use the Lua API for button styles, selects, and message builders.
For automatic behavior in one channel, continue with Channel
Automation.