Skip to content

10.8 Antrian Booking

Topologi: B — Agent + Workflow Materi yang dipakai: 4.1, 4.2, 4.3Prasyarat: 10.3

Masalahnya

Restoran menerima reservasi lewat WhatsApp. Prosesnya sudah baku dan tertulis di SOP:

  1. Validasi permintaan (jumlah orang wajar? tanggal valid?)
  2. Cek ketersediaan meja dan ketersediaan staf — dua sistem berbeda
  3. Kalau keduanya oke, konfirmasi. Kalau tidak, masukkan ke daftar tunggu
  4. Catat jejak untuk audit

Ini bukan pekerjaan agent. Kalau agent memutuskan sendiri urutannya, suatu hari ia akan mengkonfirmasi booking tanpa mengecek staf — dan restoran kelebihan tamu di malam Tahun Baru.

Tapi percakapannya tetap butuh agent: pelanggan menulis "besok malam bertiga ya" dan itu harus diterjemahkan jadi tanggal dan angka.

Keputusan arsitektur

Keputusan 1 — Pembagian agent dan workflow

Ini penerapan pertama Topologi B, dan pembagiannya adalah inti kasus ini.

LapisanTugasKenapa di situ
AgentMemahami "besok malam bertiga" → { tanggal, jumlahOrang }Butuh penafsiran bahasa
WorkflowValidasi → cek → putuskan → catatUrutan tidak boleh bervariasi

Aturannya: agent menentukan apa yang diminta, workflow menentukan bagaimana memprosesnya.

Untung pemisahan iniRugi
Proses bisnis deterministik dan bisa diauditDua komponen untuk dipahami
Bisa diuji dengan assertion biasa, tanpa scorerPerlu memetakan keluaran agent ke input workflow
Perubahan SOP tidak menyentuh instruksi agent
Alur terlihat sebagai diagram di Studio

Keputusan 2 — Cek meja dan staf: berurutan atau paralel?

.then() berurutan.parallel()
Waktu totalJumlah keduanyaYang terlama saja
Kalau cek pertama gagalCek kedua tidak jalan — hematTetap jalan — sia-sia
Kerumitan schemaSederhanaKeluaran bersarang per id

Pilihan: .parallel().

Kedua pemeriksaan memanggil sistem berbeda dan tidak saling bergantung. Berurutan berarti pelanggan menunggu dua kali lebih lama tanpa alasan.

Ongkosnya: kalau meja sudah penuh, pemeriksaan staf tetap berjalan sia-sia. Untuk dua panggilan cepat itu pertukaran yang sepadan — tapi kalau salah satu pemeriksaan mahal, berurutan justru lebih baik.

Keputusan 3 — Jejak audit: state atau dioper antar langkah?

Workflow stateDioper lewat schema
Schema tiap langkahBersihIkut membawa jejak di semua langkah
Langkah bisa dihapus/ditambahMudahMerusak rantai
Diverifikasi compilerSebagianPenuh

Pilihan: workflow state.

Ini persis kasus yang dijelaskan di 4.3: nilai yang dikumpulkan semua langkah dan dipakai di akhir. Mengopernya lewat schema berarti setiap langkah membawa field yang tidak ia pakai.

Data booking sendiri tetap lewat input/output antar langkah — karena ia mengalir antar langkah bertetangga.

Struktur direktori

text
booking-resto/
├── src/
│   ├── domain/
│   │   ├── ketersediaan.ts      ← cek meja & staf
│   │   └── antrian.ts           ← aturan waiting list
│   ├── infra/
│   │   ├── db.ts
│   │   └── whatsapp.ts
│   ├── mastra/
│   │   ├── agents/
│   │   │   └── resepsionis-agent.ts
│   │   ├── workflows/
│   │   │   └── booking-workflow.ts   ← SATU berkas, seluruh alur
│   │   ├── tools/
│   │   │   └── proses-booking.ts     ← jembatan agent → workflow
│   │   └── index.ts
│   └── server/
│       └── webhook.ts
└── package.json

Seluruh workflow ada di satu berkas. Untuk alur enam langkah itu masih terbaca, dan menyebarnya ke enam berkas justru menyembunyikan alurnya — padahal alur itulah yang paling penting dipahami.

Pecah jadi beberapa berkas saat langkahnya melewati ~10, atau saat sebuah sub-alur dipakai ulang workflow lain.

Alur

Empat pola control flow dari 4.2 dalam satu alur: sekuensial, paralel, penggabungan, dan percabangan.

Implementasi

src/mastra/workflows/booking-workflow.ts

ts
import { createWorkflow, createStep } from '@mastra/core/workflows'
import { z } from 'zod'
import { cekMejaTersedia, cekStafCukup } from '../../domain/ketersediaan.ts'

const permintaanSchema = z.object({
  nama: z.string(),
  tanggal: z.string(),
  jumlahOrang: z.number(),
})

// --- 1. Validasi ---
const validasi = createStep({
  id: 'validasi',
  inputSchema: permintaanSchema,
  outputSchema: permintaanSchema.extend({ valid: z.boolean() }),
  stateSchema: z.object({ jejak: z.array(z.string()) }),
  execute: async ({ inputData, state, setState }) => {
    await setState({ jejak: [...state.jejak, 'validasi'] })
    return {
      ...inputData,
      valid: inputData.jumlahOrang > 0 && inputData.jumlahOrang <= 20,
    }
  },
})

// --- 2a & 2b. Dua pemeriksaan paralel ---
const cekMeja = createStep({
  id: 'cek-meja',
  inputSchema: permintaanSchema.extend({ valid: z.boolean() }),
  outputSchema: z.object({ mejaTersedia: z.number() }),
  execute: async ({ inputData }) => ({
    mejaTersedia: await cekMejaTersedia(inputData.tanggal, inputData.jumlahOrang),
  }),
})

const cekStaf = createStep({
  id: 'cek-staf',
  inputSchema: permintaanSchema.extend({ valid: z.boolean() }),
  outputSchema: z.object({ stafCukup: z.boolean() }),
  execute: async ({ inputData }) => ({
    stafCukup: await cekStafCukup(inputData.tanggal),
  }),
})

// --- 3. Gabungkan hasil paralel ---
const putuskan = createStep({
  id: 'putuskan',
  // Keluaran paralel BERSARANG di bawah id tiap langkah.
  inputSchema: z.object({
    'cek-meja': z.object({ mejaTersedia: z.number() }),
    'cek-staf': z.object({ stafCukup: z.boolean() }),
  }),
  outputSchema: z.object({ bisa: z.boolean(), mejaTersedia: z.number() }),
  stateSchema: z.object({ jejak: z.array(z.string()) }),
  execute: async ({ inputData, state, setState }) => {
    const meja = inputData['cek-meja'].mejaTersedia
    const staf = inputData['cek-staf'].stafCukup
    await setState({ jejak: [...state.jejak, 'putuskan'] })
    return { bisa: meja > 0 && staf, mejaTersedia: meja }
  },
})

// --- 4a & 4b. Dua cabang, schema HARUS sama ---
const hasilSchema = z.object({ hasil: z.string() })
const keputusanSchema = z.object({ bisa: z.boolean(), mejaTersedia: z.number() })

const konfirmasi = createStep({
  id: 'konfirmasi',
  inputSchema: keputusanSchema,
  outputSchema: hasilSchema,
  stateSchema: z.object({ jejak: z.array(z.string()) }),
  execute: async ({ state, setState }) => {
    await setState({ jejak: [...state.jejak, 'konfirmasi'] })
    return { hasil: 'dikonfirmasi' }
  },
})

const antrikan = createStep({
  id: 'antrikan',
  inputSchema: keputusanSchema,
  outputSchema: hasilSchema,
  stateSchema: z.object({ jejak: z.array(z.string()) }),
  execute: async ({ state, setState }) => {
    await setState({ jejak: [...state.jejak, 'antrikan'] })
    return { hasil: 'masuk-antrian' }
  },
})

export const bookingWorkflow = createWorkflow({
  id: 'booking',
  inputSchema: permintaanSchema,
  outputSchema: hasilSchema,
  stateSchema: z.object({ jejak: z.array(z.string()) }),
})
  .then(validasi)
  .parallel([cekMeja, cekStaf])
  .then(putuskan)
  .branch([
    [async ({ inputData }) => inputData.bisa === true, konfirmasi],
    [async ({ inputData }) => inputData.bisa === false, antrikan],
  ])
  .commit()

Tiga hal yang paling sering salah, dan cara benarnya terlihat di atas.

Keluaran paralel bersarang. inputData['cek-meja'].mejaTersedia, bukan inputData.mejaTersedia. Ini penyebab undefined paling umum saat memakai .parallel().

Kedua cabang berbagi schema. keputusanSchema masuk, hasilSchema keluar — keduanya identik. Percabangan menuntut ini.

Kondisi cabang menutupi semua kemungkinan. === true dan === false bersama-sama menutup seluruh nilai boolean. Kalau cabang kedua ditulis sebagai kondisi lain yang tidak saling melengkapi, ada masukan yang lolos tanpa penanganan.

src/mastra/tools/proses-booking.ts

ts
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'

export const prosesBooking = createTool({
  id: 'proses-booking',
  description:
    'Proses permintaan reservasi meja. Panggil SETELAH nama, tanggal, dan ' +
    'jumlah orang lengkap diketahui. BUKAN untuk menanyakan ketersediaan saja.',
  inputSchema: z.object({
    nama: z.string(),
    tanggal: z.string().describe('Format YYYY-MM-DD'),
    jumlahOrang: z.number().describe('Jumlah tamu, 1-20'),
  }),
  outputSchema: z.object({
    hasil: z.enum(['dikonfirmasi', 'masuk-antrian', 'ditolak']),
  }),
  execute: async (input, { mastra }) => {
    const wf = mastra.getWorkflow('bookingWorkflow')
    const run = await wf.createRun()

    const r = await run.start({
      inputData: input,
      initialState: { jejak: [] },
    })

    if (r.status !== 'success') {
      return { hasil: 'ditolak' as const }
    }
    // Hasil branch dikunci berdasarkan id langkah yang jalan.
    const keluaran = r.result as Record<string, { hasil: string }>
    const nilai = Object.values(keluaran)[0]?.hasil
    return { hasil: (nilai === 'dikonfirmasi' ? 'dikonfirmasi' : 'masuk-antrian') as const }
  },
})

Tool ini adalah jembatan antara agent dan workflow. Ia menerjemahkan hasil percakapan menjadi input workflow, lalu hasil workflow menjadi sesuatu yang bisa disampaikan agent.

Perhatikan pemeriksaan r.status !== 'success' — pola wajib dari 4.1.

src/mastra/agents/resepsionis-agent.ts

ts
import { Agent } from '@mastra/core/agent'
import { Memory } from '@mastra/memory'
import { prosesBooking } from '../tools/proses-booking.ts'

export const resepsionisAgent = new Agent({
  id: 'resepsionis-agent',
  name: 'Resepsionis',
  instructions: `Kamu resepsionis restoran yang melayani reservasi lewat WhatsApp.

Kumpulkan tiga hal sebelum memproses: nama, tanggal, dan jumlah orang.
Kalau ada yang belum jelas, tanyakan — satu pertanyaan per pesan.

Ubah tanggal relatif ("besok", "sabtu depan") menjadi format YYYY-MM-DD.
Hari ini ${new Date().toISOString().slice(0, 10)}.

Setelah ketiganya lengkap, panggil prosesBooking.

Sampaikan hasilnya apa adanya:
- dikonfirmasi: beri selamat, sebutkan tanggal dan jumlah orang
- masuk-antrian: jelaskan bahwa penuh dan mereka masuk daftar tunggu
- ditolak: minta maaf dan sarankan menghubungi restoran langsung

JANGAN menjanjikan meja sebelum prosesBooking mengembalikan "dikonfirmasi".`,
  model: 'openai/gpt-5-mini',
  tools: { prosesBooking },
  memory: new Memory({ options: { lastMessages: 10 } }),
})

Kalimat terakhir adalah pengaman bisnis. Tanpa itu, agent yang ramah bisa berkata "baik, meja untuk 4 orang sudah saya siapkan!" sebelum workflow memastikan mejanya ada.

Hasil nyata

Workflow dijalankan sungguhan pada dua skenario. Tidak ada panggilan model di sini — workflow bersifat deterministik.

text
=== Sari, 2026-10-10, 4 orang ===
   validasi: 4 orang pada 2026-10-10
   cek-meja (paralel)
   cek-staf (paralel)
   putuskan: meja=5 staf=true -> BISA
   konfirmasi: booking DITERIMA
   STATUS: success
   HASIL : {"konfirmasi":{"hasil":"dikonfirmasi"}}

=== Budi, 2026-12-31, 6 orang ===
   validasi: 6 orang pada 2026-12-31
   cek-meja (paralel)
   cek-staf (paralel)
   putuskan: meja=0 staf=true -> PENUH
   antrikan: masuk WAITING LIST
   STATUS: success
   HASIL : {"antrikan":{"hasil":"masuk-antrian"}}

Tiga hal yang terbukti:

1. Urutan selalu sama. Kedua skenario menjalankan validasi → paralel → putuskan, tanpa variasi. Itu yang tidak bisa dijamin agent.

2. Hanya satu cabang jalan. Skenario pertama tidak menyentuh antrikan sama sekali, dan sebaliknya.

3. Hasil dikunci berdasarkan id cabang yang jalan. Perhatikan bentuknya: {"konfirmasi":{...}} versus {"antrikan":{...}} — bukan objek datar.

Bentuk hasil branch mudah mengejutkan

Kode pemanggil tidak bisa menulis r.result.hasil — kuncinya adalah id langkah yang kebetulan jalan, dan itu berbeda antar eksekusi.

Karena itu proses-booking.ts memakai Object.values(keluaran)[0]. Alternatif yang lebih eksplisit: periksa keberadaan tiap kunci yang mungkin.

Perhatikan juga skenario Budi: STATUS: success meski bookingnya penuh. Workflow berhasil — yang "gagal" adalah bookingnya, dan itu hasil bisnis yang sah, bukan error teknis. Membedakan keduanya penting.

Best practice yang dipakai di sini

1. Satu berkas untuk seluruh alur. Alurnya sendiri adalah informasi paling penting; menyebarnya ke enam berkas menyembunyikannya.

2. Schema bersama untuk cabang. keputusanSchema dan hasilSchema didefinisikan sekali dan dipakai kedua cabang — mustahil tidak sinkron.

3. Jejak audit lewat state. Langkah bisa ditambah atau dihapus tanpa merusak rantai schema.

4. Tool sebagai jembatan. Agent tidak memanggil workflow langsung; ada satu tool yang menerjemahkan keduanya.

5. Larangan menjanjikan hasil di instruksi agent. Agent hanya boleh melaporkan apa yang dikembalikan workflow.

6. Status teknis dibedakan dari hasil bisnis. status: 'success' + hasil: 'masuk-antrian' adalah kombinasi yang benar.

Kapan pola ini salah

SituasiKenapa gagalKe mana
Booking > 10 orang perlu persetujuan manajerWorkflow harus berhenti menunggu manusia10.9 Approval
Perlu memproses 50 permintaan sekaligusSatu per satu terlalu lambat.foreach() dengan concurrency
Cek ketersediaan makan 30 detikMelewati batas wajar respons chatBackground task, 7.1
SOP berubah tiap cabang restoranSatu workflow tidak cukup10.10 Multi-tenant

Coba sendiri

Perluas workflow ini dengan .foreach(): setelah booking dikonfirmasi, kirim notifikasi ke setiap tamu dalam rombongan yang punya nomor terdaftar.

Tambahkan juga langkah validasi yang menolak — bukan mengantrikan — kalau jumlah orang di luar 1–20.

Selesai kalau

  • [ ] Alur memakai keempat pola: .then(), .parallel(), .branch(), .foreach()
  • [ ] Tiga cabang berbeda bisa dicapai: dikonfirmasi, antrian, ditolak
  • [ ] .foreach() memakai concurrency selain default, dan kamu bisa menjelaskan angkanya
  • [ ] jejak di akhir memuat semua langkah yang benar-benar jalan
  • [ ] Tidak ada masukan yang membuat workflow berhenti tanpa cabang terpilih
  • [ ] Kamu bisa menjalankan seluruh workflow tanpa memanggil model

Kalau macet

Langkah setelah .parallel() menerima undefined? Keluarannya bersarang. Deklarasikan inputSchema dengan id tiap langkah paralel sebagai kunci.

Error tipe di .branch()? Kedua cabang harus punya inputSchema dan outputSchema yang identik. Definisikan sekali sebagai konstanta.

Workflow berhenti tanpa hasil? Tidak ada kondisi cabang yang terpenuhi. Pastikan kondisi-kondisinya menutupi seluruh kemungkinan.

Lanjut ke mana

Workflow ini berjalan dari awal sampai akhir tanpa henti. Kasus berikutnya menambahkan kemampuan yang paling khas dari workflow: berhenti menunggu manusia, lalu dilanjutkan berhari-hari kemudian dari proses yang berbeda — 10.9 Approval Pengeluaran.

Materi belajar mandiri. Bukan dokumentasi resmi Mastra.