K
Docs
Building Bots

🔗 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.

🪄
/ping
Spell fires
💬
Reply
base.reply — Pong!

A node with no outgoing connection is a terminal node — execution ends there.

📐Under the hood — a flow is just an async function
TypeScript
// 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.

🔀
Condition
{{score}} > 100
true
💬Reply: You won! 🎉
false
💬Reply: Keep going…
💡Good to know

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.

🔀
Condition
base.condition
💬
Reply A · Reply B
true / false branches
🔁
Merge
base.merge — both branches join here
📝
Write to Vault
base.db.write

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.

🪄
/award (optional: target)
🌊
Coalesce
primary: {{input.options.user_p.id}} → fallback: {{input.user.id}}
🔄
Write score
base.db.write — upsert on
📐Under the hood — coalesce is the ?? operator
TypeScript
// 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: greeting
Hello, {{input.user.name}}!
💬
Reply
content: {{greeting}}
📐Under the hood — variables are just... variables
TypeScript
// Save Spark (base.variable) stores a named value:
// config: name = "greeting", value = "Hello!"

// {{greeting}} is available in every downstream piece.
📌Note

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.

What's in the context
input.* / member.* / message.*Context from the trigger or spell
Named sparksFrom Save Spark, Read Vault (as), Loop item, etc.
lib.*Library sparks from Booster Packs

Each 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.