🧩 Pieces
Every Piece on the Canvas — documented with config fields, output ports, sparks they add, JSON examples, and a TypeScript equivalent so you understand exactly what each one does.
Messages
base.replyTalks back! On spells, replies under the slash command (optionally private). On message triggers, replies in a thread under the message that fired the flow.
Base Setbase.followupSends an additional message under the same slash command. Discord only allows one initial reply per spell — use Reply for the first message, then Follow-up for every message after.
Base Setbase.sendDrops a message anywhere in your server. Unlike Reply, it doesn't need to be a response to anything — it can post in #announcements while the trigger happened in #general.
Base Setbase.embedSends a fancy embed card — title, body text, accent colour, a big image, a thumbnail, and an author and footer with little icons. Image fields take a link, an avatar like {{user.avatar}}, an uploaded file, a gallery:label, or an image your flow made. Great for leaderboards, announcements, profiles and summaries.
Base Setbase.dmSends a private DM directly to a specific user. No one else sees it. Perfect for sending private results, warnings, or personalized info. Note: some users have DMs disabled — that's what the Error port is for.
Base SetLogic
base.conditionThe decision maker! Checks if something is true or false, then sends the flow down one of two paths. It's how your bot becomes smart — 'if this user is an admin, do X, otherwise do Y'.
Base Setbase.mergeA visual hub for converging flows. Wire both Condition outputs (or any parallel paths) into this piece, then connect one output to your shared downstream chain. The runner already supports multiple incoming edges — Merge makes that pattern obvious on the canvas.
Base Setbase.coalesceSets a spark from a primary template, or falls back when the primary is empty. Perfect for /profile user_p: use the mentioned user's ID when provided, otherwise the invoker's ID — without duplicating Save Sparks on each branch.
Base Setbase.waitPumps the brakes! Pauses the flow for a set number of milliseconds before continuing. Useful for adding a dramatic pause before a big reveal, or spacing out multiple messages so they don't all appear at once.
Base Setbase.variableCreates or updates a named spark that holds a typed value — text, number, yes/no, list, or object. Update existing sparks in place: numbers use Add/Subtract/Multiply/Divide, text uses append/prepend, lists use Add to list / Prepend to list (single items or rows), booleans support toggle. The spark is then available everywhere downstream.
Base Set{{score}}base.loopRepeat after me! Takes a list and runs the connected flow once for each item in it. Great for sending messages to multiple users, processing a list of scores, or doing anything that needs to happen more than once.
Base Set{{item}}base.returnEnds the custom piece's flow and hands named values back to whoever called it. The fields shown here come from the returns you defined on this custom piece.
Base SetVault
base.db.readFetches rows from one of your Vault tables. Pick a table, choose which column to match on (like user_id), and give the result a name — it becomes a ✨ Spark you can drop into any later piece.
The Vault{{result}}{{result.length}}{{result.0}}base.db.writeSaves a row to one of your Vault tables. Pick a table, fill in each column's value, and optionally turn on 'update if exists' to avoid duplicate rows — perfect for tracking coins, XP, or anything per-user.
The Vaultbase.db.deletePermanently removes rows from one of your Vault tables. Pick the table, choose which column to match on, and only those rows get deleted. No filter? The whole table is wiped — so be careful!
The Vaultvault.safeA safe for your Vault changes. Put Vault pieces on its Inside path and they count as one: if everything works, every change is saved at once and the flow carries on from All saved. If anything goes wrong — or a ✋ Call it Off runs — every change inside is undone like it never happened, and the flow takes Undone instead. Perfect for shops, trades and paying someone: nobody ends up with coins from nowhere.
The Vaultvault.abortChanged your mind? Inside an 🔐 All-or-Nothing box, this undoes every Vault change the box made and jumps to its Undone path, carrying your reason along as {{oops.reason}}. Pair it with a Condition: "not enough coins? call it off".
The Vaultvault.addChanges one number on one row — coins, XP, lives, streaks — in a single step nobody can sneak between. Set "Never go below" and it simply refuses to go under it: perfect for spending, since two clicks at once can't double-spend. If the row doesn't exist yet it's created for you, starting from the column's default.
The Vaultvault.getLike Read from Vault, but for when you want one row: the first one that matches. Its columns become sparks directly — {{row.coins}} instead of {{result.0.coins}}. Filter with = ≠ > < ≥ ≤, contains or starts with.
The Vaultvault.topSorts a table by one column and hands back the top rows — a leaderboard in one piece. Each row gets a place number too ({{item.place}} in a Loop), and empty values always sort last. Filter it to make per-team or per-server boards.
The Vaultvault.rankFinds one row's place when the table is sorted by a column — without fetching the whole leaderboard. Ties share a place, like a real scoreboard. Gives you {{rank.place}}, {{rank.total}} and {{rank.value}}.
The Vaultvault.tallyAdds things up across a table: how many rows, the total of a column, its average, lowest or highest. Filter it to tally just one team, one server or one kind of thing. Great for "coins in circulation" or "how many warnings has this person had".
The Vaultvault.randomPulls one row out of a table at random — a random quote, a random winner, a random card from a deck. Filter it to pick only from some rows. Its columns become sparks: {{pick.text}}.
The Vaultvault.updateChanges the rows that match your filter, all at once: reset everyone's weekly score, move a whole team, add 5 XP to every member of a guild. Number columns can + Add or − Sub just like Write to Vault. It never creates rows, so nothing appears out of thin air.
The Vaultvault.cooldownLets something happen once per time window: a /daily reward, a /work shift, a /rob that needs to cool off. The first time it's Ready and starts the timer; until the time is up it's Cooling, with {{cooldown.left}} saying how long to wait. No table needed — the Vault keeps track for you.
The VaultUtility
base.randomReaches into a bag and pulls out one random item. Great for random jokes, quotes, prizes, challenge assignments, or anything where variety is the spice of life.
Base Set{{pick}}base.logWrites a message to your bot's Activity log. Use info for breadcrumbs, warn for odd-but-okay moments, and error when something went wrong but you handled it in the flow. Great for debugging without spamming Discord.
Base Setbase.formatConstructs a piece of text by filling in a template with live values. Combine static text with Sparks to build dynamic messages, labels, or anything that needs to be assembled from parts.
Base Set{{formatted}}base.format_timeFormats a date/time spark into human-readable text, ISO, a custom pattern, or Discord's native `<t:unix:style>` timestamps (t, T, d, D, f, F, R). Leave the input empty to use the current time when the flow runs.
Base Setbase.mathRuns an arithmetic expression and stores the answer as a Spark. Add scores, calculate percentages, multiply rewards — if numbers are involved, this piece does the work.
Base Set{{total}}base.sortTakes a list and sorts it ascending or descending. If the list came from a Vault read, it auto-detects the table schema and lets you pick which column to sort by — no guessing field names. The sorted result becomes a ✨ Spark you can loop over or show directly.
Base Set{{sorted}}{{sorted.length}}{{sorted.0}}base.text.splitSplits text into a list using a delimiter — comma, space, newline, or any string. Great for parsing command arguments, CSV-style data, or multi-line input.
Base Set{{parts}}{{parts.length}}{{parts.0}}base.text.joinJoins list items into a single string with a separator between each item. Perfect for building comma-separated lists, bullet lines, or one big message from many sparks.
Base Set{{joined}}base.text.replaceReplaces every occurrence of a find string with another value. Handy for sanitizing input, swapping placeholders, or cleaning up user text.
Base Set{{replaced}}base.text.transformTransforms text casing or whitespace. Use trim before comparing strings, uppercase for shouty replies, or title case for display names.
Base Set{{transformed}}base.text.lengthReturns how many characters are in a piece of text. Useful for validating message length, word-count style games, or limits before sending.
Base Set{{length}}base.list.lengthReturns how many items are in a list spark. Pair with Condition to branch on empty lists or check leaderboard size.
Base Set{{count}}base.list.getReturns the item at a zero-based index — 0 is first, 1 is second, and so on. Great for showing the top scorer or a random pick you already stored.
Base Set{{row}}base.list.sliceReturns a slice of a list from a start index up to (but not including) an end index. Use it for top-10 boards, paging, or trimming edges.
Base Set{{slice}}{{slice.length}}{{slice.0}}base.list.uniqueDeduplicates a list while keeping the first occurrence of each value. Handy after merges, user submissions, or tag lists.
Base Set{{unique}}{{unique.length}}{{unique.0}}base.list.containsChecks whether a list contains an item (string match). Routes to Found when present, Empty when not — like a mini condition for lists.
Base SetChannels
User
base.member.has_permissionBranches based on whether a server member has a specific Discord permission (like Ban Members or Manage Messages). Use it in message flows to skip filters for mods, or to gate logic on someone other than the command invoker.
Base Setbase.role.addHands out a Discord role to a user. Perfect for level-up rewards, verifications, unlock systems, or any time someone earns access to something. The bot needs Manage Roles permission for this to work.
Base Setbase.role.removeThe flip side of Add Role — takes a role away from a user. Use it for role rotation, timed access, punishments, or when someone no longer meets the criteria for a role.
Base Setbase.member.fetchFinds a member of your server by their Discord user ID and loads their info — username, roles, join date, and more — as a Spark you can use anywhere. Useful when you need data about someone other than whoever triggered the flow.
Base Set{{member}}{{member.name}}Moderation
mod.kickRemoves a member from the server — they can still rejoin with an invite. A lighter touch than a ban; great for first offences or automated flows. Always pair with a Condition to confirm the boot is warranted.
The Mod Squadmod.banPermanently bans a user. Unlike Kick, they cannot rejoin. Optionally deletes their recent messages (1–7 days). Use with care — you can always Unban later if needed.
The Mod Squadmod.unbanRemoves a ban by user ID. They won't automatically rejoin — they'll need a fresh invite. Perfect for /unban commands where a mod reinstates a previously banned member.
The Mod Squadmod.timeoutPuts a member in Discord's native timeout. While timed out they can't send messages, add reactions, or join voice. Perfect for automated cool-downs. Requires Moderate Members permission. Max duration is 28 days.
The Mod Squadmod.untimeoutClears an active timeout and restores a member's ability to chat. Handy for manual override commands — a mod can un-mute someone before their timeout would naturally expire.
The Mod Squadmod.delete_messageDeletes one specific message from a channel. Use in message-event flows where bad content needs to go immediately. The message ID and channel ID are available as Sparks in message triggers.
The Mod Squadmod.purgeDeletes up to 100 messages from a channel in one sweep. Discord only allows bulk-deleting messages under 14 days old. Perfect for cleaning up spam raids, accidental leaks, or bot noise.
The Mod Squadmod.slowmodeSets a channel's slowmode cooldown — from 0 (off) to 21600 seconds (6 hours). Users can only send one message per cooldown. Great for calming heated chats, preventing raid spam, or managing high-traffic events.
The Mod Squadmod.lock_channelRemoves @everyone's Send Messages permission in a channel — effectively locking it. The channel remains visible and readable; nobody can post until you Unlock it. Great for announcements-only mode or emergency lockdowns.
The Mod Squadmod.unlock_channelRe-enables @everyone's Send Messages permission, reversing a Lock Channel. When the dust settles, unlock and get the conversation flowing again.
The Mod SquadControls
ui.buttonAdds a button to whatever message you send next — Reply, Embed, Send or Follow-up all pick it up. Give it a custom id, then make a Button Pressed trigger with the same id to run a flow when someone clicks it. Five buttons fit on a row; after that they wrap.
The Control Panelui.rowButtons pack five to a row on their own. Drop this between two Add Button pieces when you want the layout broken up sooner — handy for grouping actions.
The Control Panelui.selectAdds a dropdown to the next message, with choices you type in. Pair it with a Dropdown Chosen trigger to run a flow when someone picks — what they picked arrives as {{component.value}}.
The Control Panelui.select.userAdds a dropdown Discord fills with members of the server — no choices to write. The chosen member's ID comes back as {{component.value}}.
The Control Panelui.select.roleAdds a dropdown Discord fills with the server's roles. The chosen role ID comes back as {{component.value}} — perfect for self-serve role menus.
The Control Panelui.select.channelAdds a dropdown Discord fills with the server's channels, optionally narrowed to one kind. The chosen channel ID comes back as {{component.value}}.
The Control Panelui.select.mentionableAdds a dropdown listing both members and roles, for when either answer would do. The chosen ID comes back as {{component.value}}.
The Control Panelui.text_inputAdds one input box to the pop-up form you open next. Put one of these before Show Pop-up Form for each thing you want to ask. Whatever gets typed comes back to your Form Submitted trigger as {{form.values.<custom id>}}.
The Control Panelui.modalOpens a pop-up form built from the Add Form Field pieces before it. This has to be the very first thing a flow does — Discord won't show a form after the bot has already replied, so don't put a Reply, a Wait, or any slow piece ahead of it. Catch the answers with a Form Submitted trigger.
The Control Panelui.updateRewrites the message the button or dropdown lives on, instead of posting a new one. Stage new buttons before this piece to swap them out; stage none and the existing ones stay put. The classic 'you voted!' in-place update.
The Control Panelui.disableLeaves every button and dropdown on the message visible but dead. The one-line fix for people clicking twice, or voting after the poll closed.
The Control Panelui.clearTakes every button and dropdown off the message, leaving the text behind. Good for one-shot prompts that shouldn't linger.
The Control PanelImages
img.avatarFetches a member's avatar as an image spark you can drop into the Studio, touch up, or attach. Leave Whose avatar empty for whoever triggered the flow — or whoever got right-clicked. Animated avatars stay animated unless you freeze them.
The Darkroomimg.server_iconFetches this server's icon, banner or invite splash as an image spark. Works on Interaction Only bots too — Kitcord looks the server up on demand.
The Darkroomimg.from_messageCollects the images on a message — uploads, embed images and stickers. Use the message that triggered the flow, the one that was right-clicked, the one it replies to, or any message link. You get the first (or last) image plus a list of all of them.
The Darkroomimg.from_urlDownloads a PNG, JPEG, GIF or WebP from any https link — handy for APIs that return image URLs. Links to private networks are blocked, and files over 10 MB are refused.
The Darkroomimg.galleryFetches an image from this bot's Gallery tab — by its label, or by tag (a random one, the newest, the oldest, or all of them). Upload new images any time; flows see them instantly, no republish needed.
The Darkroomimg.inspectLooks at an image and tells your flow about it: width, height, format, whether it's animated, its dominant colour, a 5-colour palette and how bright it is. Branches on whether it's an image at all — perfect for validating uploads.
The Darkroomimg.studioOpens the Studio: lay out avatars, text, shapes and progress bars on a canvas, drop in sparks like {{user.displayName}} or {{vars.xp}}, and the bot develops a fresh image every run. If an avatar or Gallery image in the design is animated, the result is an animated GIF.
The Darkroomimg.editRuns a stack of quick edits on an image, top to bottom: resize, crop to a circle, rotate, adjust colours, add a border, slap on meme captions, deep-fry it, or speed up / reverse / boomerang a GIF. Animated GIFs stay animated.
The Darkroomimg.gifStitches up to 30 images into an animated GIF, one after another. Feed it a list spark (like every image on a message, or every Gallery image with a tag), or one image per line.
The Darkroomimg.attachAttaches an image to whatever message you send next — Reply, Embed, Follow-up, Send, DM, or Update the Message all pick it up, just like buttons do. Add alt text for screen readers, or hide it behind a spoiler.
The Darkroom