Lesson 3 / 7 · 12 min
Sessions and the order flow state machine
SQLite sessions and a real multi-step order flow. Checkpoint: the bot takes a complete order, step by step.
An echo bot forgets you instantly. To take an order the bot needs memory (which step is this customer on) and rules (what to ask next). Memory is a sessions table; rules are a state machine. Both fit in two small files.
Step 1: the database (db.js)
import Database from "better-sqlite3";
export const db = new Database("data/bot.db");
db.exec(
"CREATE TABLE IF NOT EXISTS sessions(" +
" phone TEXT PRIMARY KEY," +
" state TEXT NOT NULL DEFAULT 'START'," +
" data TEXT NOT NULL DEFAULT '{}'," +
" updated_at INTEGER);" +
"CREATE TABLE IF NOT EXISTS processed(" +
" message_id TEXT PRIMARY KEY);"
);
export function getSession(phone) {
const row = db
.prepare("SELECT * FROM sessions WHERE phone = ?")
.get(phone);
if (!row) return { phone, state: "START", data: {} };
return { phone, state: row.state, data: JSON.parse(row.data) };
}
export function saveSession(s) {
db.prepare(
"INSERT INTO sessions(phone, state, data, updated_at) " +
"VALUES (?, ?, ?, unixepoch()) " +
"ON CONFLICT(phone) DO UPDATE SET " +
"state = excluded.state, data = excluded.data, " +
"updated_at = excluded.updated_at",
).run(s.phone, s.state, JSON.stringify(s.data));
}Step 2: the state machine (flow.js)
const MENU =
"Halo! Balas dengan angka ya:\n" +
"1. Menu & harga\n2. Buat pesanan\n3. Chat admin";
const PRICES = { A: 85000, B: 120000 };
export function step(session, text) {
const t = text.trim().toLowerCase();
if (t === "admin") return go("HUMAN", "Oke, admin akan membalas di sini ya.");
switch (session.state) {
case "START":
return go("MENU", MENU);
case "MENU":
if (t === "1") return go("MENU", "Paket A Rp 85.000, Paket B Rp 120.000. Balas 2 untuk pesan.");
if (t === "2") return go("ITEM", "Mau Paket A atau Paket B?");
if (t === "3") return go("HUMAN", "Oke, admin akan membalas di sini ya.");
return go("MENU", MENU);
case "ITEM": {
const item = t === "a" || t === "paket a" ? "A"
: t === "b" || t === "paket b" ? "B" : null;
if (!item) return go("ITEM", "Balas A atau B ya.");
session.data.item = item;
return go("QTY", "Berapa box?");
}
case "QTY": {
const qty = parseInt(t, 10);
if (!qty || qty < 1 || qty > 500)
return go("QTY", "Tulis angka saja ya, contoh: 2");
session.data.qty = qty;
session.data.total = qty * PRICES[session.data.item];
return go("ADDRESS", "Alamat pengirimannya ke mana?");
}
case "ADDRESS":
session.data.address = text.trim();
return go(
"CONFIRM",
session.data.qty + " box Paket " + session.data.item +
" ke " + session.data.address +
". Total Rp " + session.data.total.toLocaleString("id-ID") +
". Balas YA untuk lanjut bayar, atau UBAH."
);
case "CONFIRM":
if (t === "ya") return go("PAYMENT", null, "create_invoice");
if (t === "ubah") return go("MENU", MENU);
return go("CONFIRM", "Balas YA atau UBAH ya.");
default:
return go("MENU", MENU);
}
function go(state, reply, action) {
session.state = state;
return { session, reply, action };
}
}Step 3: wire it into the webhook
// server.js
import { getSession, saveSession } from "./db.js";
import { step } from "./flow.js";
import { sendText } from "./wa.js";
app.post("/webhook", async (req, res) => {
res.sendStatus(200);
const msg = req.body?.entry?.[0]?.changes?.[0]?.value?.messages?.[0];
if (!msg || msg.type !== "text") return;
const session = getSession(msg.from);
if (session.state === "HUMAN") return; // a person owns this chat now
const { reply } = step(session, msg.text.body);
saveSession(session);
if (reply) await sendText(msg.from, reply);
});Checkpoint: walk the full flow from your phone, menu, item, quantity, address, confirmation, including wrong inputs (letters where a number belongs). The bot should re-ask politely and never crash. That resilience is the difference between a demo and a product.
What you take away
- Sessions in SQLite give the bot memory across messages, keyed by phone
- The state machine validates every input and re-asks instead of crashing
- 'admin' escapes to a human from any state; HUMAN state silences the bot
- Load, step, save, reply is the universal chatbot loop
Curated sources
Hand-picked references for going deeper, reviewed by our team.