🔗 Flows & Canvas
A Flow is a connected graph of Piece nodes. When a Spell or Trigger fires, the Engine starts at the entry node and follows edges until it reaches a terminal node (one with no outgoing connections).
↓Linear Flows
The simplest flows are linear — each node connects to exactly one next node, forming a chain.
A node with no outgoing connection is a terminal node — execution ends there.
// A Flow is just an async function. Nodes are statements; edges define order.
async function flow_welcome(member: GuildMember) {
// node_lookup → base.db.read (as: "result"):
const rows = await db.all("SELECT * FROM members WHERE user_id = ?", [member.id]);
// node_check → base.condition on {{result.length}}
if (rows.length > 0) {
await channel.send(`Welcome back, ${member.user.username}!`);
} else {
await channel.send(`Hey ${member.user.username}, welcome! 🎉`);
await db.run("INSERT INTO members (user_id) VALUES (?)", [member.id]);
}
}
// Edges connect nodes. base.condition's true/false ports = the if/else branches.🔀Branching
Certain pieces — Condition, Has Permission — emit on a true or false port. Connect edges from each port to divergent paths.
The Engine follows only the edge matching the port that was emitted. Both branches run independently — use base.merge if you want them to rejoin downstream.
🔁Merging Branches
Use base.merge when both branches of a condition should continue to the same downstream node.
base.mergehas no config — it's a passthrough hub. Connect both branch outputs to it, then connect it to whatever comes next.
🌊Coalesce (Optional Inputs)
When a spell has an optional USER input, use base.coalesce to default to the invoker if it wasn't provided.
// base.coalesce is the nullish coalescing operator (??):
const targetId =
interaction.options.getUser("user_p")?.id // primary — use if provided
?? interaction.user.id; // fallback — use if not📝Flow sparks
Set values with Save Spark (base.variable), then use {{name}} in any downstream piece.
// Save Spark (base.variable) stores a named value:
// config: name = "greeting", value = "Hello!"
// {{greeting}} is available in every downstream piece.Variables persist for the lifetime of a single flow execution — they reset on the next run. For persistent data, use the Vault.
🌐Execution Context
As the Engine walks a flow, it carries a context — all the sparks available to each piece's config.
input.* / member.* / message.*Context from the trigger or spellNamed sparksFrom Save Spark, Read Vault (as), Loop item, etc.lib.*Library sparks from Booster PacksEach piece may add sparks — e.g. Read from Vault with as: "result" adds {{result}} and {{result.0.column}}. All nodes downstream see everything added so far.