CourseWhatsApp Automation for Business
ModuleBuilding the bot
Duration12 min
Progress0/7
Course material0/7 · 0%
  1. 1What we are building, and the project setup8 min
  2. 2Connecting to the WhatsApp Cloud API12 min
  3. 3Sessions and the order flow state machine12 min
  4. 4The mini CRM: contacts, orders, and labels10 min
  5. 5Payments in chat with the Midtrans sandbox12 min
  6. 6Hardening: dedupe, retries, and human takeover10 min
  7. 7Deploying with Docker, off the laptop for good12 min

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));
}
Create a data/ folder first. One upsert and one select are the bot's entire memory layer.

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 };
  }
}
Every state validates its input, re-asks once on bad input, and 'admin' escapes from anywhere. The PAYMENT action is wired in lesson 5.

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);
});
Load, step, save, reply: the loop at the heart of every chatbot you will ever build.

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.

Related Etzal serviceWhatsApp AI chatbotPrefer it done for you? See how we help.
Sessions and the order flow state machine - WhatsApp Automation for Business | Etzal Academy | Etzal Group