Cara Membuat Aplikasi Antrian Nomor dengan Google Apps Script
8 menit baca · Diperbarui 2026-07-22
Antrian manual pakai kertas atau mesin tiket itu ribet dan mahal buat klinik kecil, bengkel, atau lapak UMKM. Padahal kebutuhannya sederhana: orang ambil nomor, layar nunjukin nomor yang lagi dipanggil, petugas klik panggil berikutnya, dan tiap pagi nomornya balik ke 1. Semua itu bisa jalan di atas Google Spreadsheet plus satu Web App Apps Script, tanpa biaya server.
Di panduan ini kita bangun tiga bagian yang berbagi satu sumber data: halaman pengunjung buat ambil nomor dari HP, halaman petugas buat memanggil, dan penghitung yang reset otomatis tiap hari. Client memanggil server pakai google.script.run (bukan fetch), dan layar menyegarkan diri sendiri lewat polling ringan tiap beberapa detik.
Kenapa Apps Script cukup untuk antrian
Spreadsheet berperan dua hal sekaligus: log tiap tiket (nomor berapa, jam berapa) dan sumber data yang gampang kamu buka manual kalau ada yang aneh. Web App-nya bisa dibuka dari HP mana pun tanpa install apa-apa, cukup satu URL.
- Spreadsheet menyimpan log tiap nomor yang diambil: kolom Nomor, Waktu, Status.
- PropertiesService menyimpan penghitung hidup (nomor terakhir diambil, nomor sedang dipanggil, tanggal aktif), jadi polling status tidak perlu buka Sheet tiap detik.
- doGet melayani halaman HTML ke browser sebagai Web App.
- google.script.run jadi jembatan dari client ke fungsi server, tanpa REST API atau fetch.
Rancang datanya dulu
Sebelum nulis kode, tentukan skema. Ini juga inti mode Web GAS: struktur tab Spreadsheet itulah skema datamu. Untuk antrian, satu sheet log sudah cukup. Buat sheet bernama Antrian, isi baris 1 dengan header Nomor, Waktu, Status. Penghitung yang berubah tiap detik sengaja tidak ditaruh di sel, tapi di Script Properties, biar cepat dan aman dari balapan tulis.
Kalau kamu belum terbiasa memindahkan data antara kode dan Sheet, prinsipnya baca dan tulis borongan pakai getValues dan setValues, bukan getValue di dalam loop. Dasarnya ada di baca dan tulis data Sheet, dan cara mengubah script jadi web bisa diakses dari HP ada di bikin Web App dari Spreadsheet.
Kode server (Code.gs)
Di server kita punya lima fungsi: doGet buat memilih halaman, bacaState_ yang sekaligus menangani reset harian, ambilNomor dan panggilBerikutnya yang menulis (dikunci LockService), serta getStatus yang cuma membaca. LockService penting di ambilNomor: kalau dua orang pencet Ambil Nomor pada detik yang sama, tanpa lock keduanya bisa dapat nomor yang sama. waitLock membuat mereka antre sepersekian detik.
// ==== Konfigurasi ====
const SPREADSHEET_ID = 'GANTI_DENGAN_ID_SPREADSHEET_KAMU';
const NAMA_SHEET = 'Antrian';
const ZONA = 'Asia/Jakarta';
const TOKEN_PETUGAS = 'ganti-token-acak-yang-panjang';
function doGet(e) {
const adminOk = e && e.parameter && e.parameter.token === TOKEN_PETUGAS;
const namaFile = adminOk ? 'petugas' : 'index';
return HtmlService.createHtmlOutputFromFile(namaFile)
.setTitle('Antrian')
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
function sheetAntrian_() {
return SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(NAMA_SHEET);
}
// Baca penghitung. Kalau tanggalnya sudah ganti hari, reset ke 0.
function bacaState_() {
const props = PropertiesService.getScriptProperties();
const hariIni = Utilities.formatDate(new Date(), ZONA, 'yyyy-MM-dd');
if (props.getProperty('tanggal') !== hariIni) {
props.setProperties({
tanggal: hariIni,
terakhirDiambil: '0',
sedangDipanggil: '0'
});
}
return {
tanggal: hariIni,
terakhirDiambil: Number(props.getProperty('terakhirDiambil')) || 0,
sedangDipanggil: Number(props.getProperty('sedangDipanggil')) || 0
};
}
// Dipanggil dari HP pengunjung.
function ambilNomor() {
const lock = LockService.getScriptLock();
lock.waitLock(10000);
try {
const state = bacaState_();
const nomorBaru = state.terakhirDiambil + 1;
PropertiesService.getScriptProperties()
.setProperty('terakhirDiambil', String(nomorBaru));
const waktu = Utilities.formatDate(new Date(), ZONA, 'yyyy-MM-dd HH:mm:ss');
sheetAntrian_().appendRow([nomorBaru, waktu, 'menunggu']);
return { nomorAnda: nomorBaru, sedangDipanggil: state.sedangDipanggil };
} finally {
lock.releaseLock();
}
}
// Dipanggil dari halaman petugas.
function panggilBerikutnya() {
const lock = LockService.getScriptLock();
lock.waitLock(10000);
try {
const state = bacaState_();
if (state.sedangDipanggil >= state.terakhirDiambil) {
return { sedangDipanggil: state.sedangDipanggil, habis: true };
}
const berikut = state.sedangDipanggil + 1;
PropertiesService.getScriptProperties()
.setProperty('sedangDipanggil', String(berikut));
return { sedangDipanggil: berikut, habis: false };
} finally {
lock.releaseLock();
}
}
// Dipanggil terus-menerus oleh polling. Hanya baca Properties, ringan.
function getStatus() {
const state = bacaState_();
return {
sedangDipanggil: state.sedangDipanggil,
terakhirDiambil: state.terakhirDiambil,
menunggu: Math.max(0, state.terakhirDiambil - state.sedangDipanggil)
};
}Perhatikan getStatus tidak menyentuh Sheet sama sekali, cuma baca Properties, jadi aman dipanggil tiap beberapa detik oleh banyak HP. Sheet hanya ditulis saat orang benar-benar ambil nomor. Reset harian pun gratis: bacaState_ melihat tanggal berubah lalu mengembalikan penghitung ke 0 pada permintaan pertama di hari baru.
Halaman pengunjung dan polling ringan
Dari client kamu tidak memanggil fungsi server pakai fetch atau XMLHttpRequest. Apps Script menyediakan google.script.run: tulis nama fungsi server langsung dan pasang withSuccessHandler untuk menerima hasilnya. Layar nomor dipanggil kita segarkan dengan setInterval yang memanggil getStatus tiap 4 detik. Simpan kode berikut sebagai file index.html di project yang sama.
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Ambil Nomor Antrian</h1>
<p>Nomor dipanggil sekarang: <b id="dipanggil">-</b></p>
<p>Sisa menunggu: <span id="menunggu">-</span></p>
<button id="btnAmbil">Ambil Nomor</button>
<p id="hasil"></p>
<script>
var btn = document.getElementById('btnAmbil');
btn.addEventListener('click', function () {
btn.disabled = true;
google.script.run
.withSuccessHandler(function (res) {
document.getElementById('hasil').textContent =
'Nomor Anda: ' + res.nomorAnda;
btn.disabled = false;
})
.withFailureHandler(function (err) {
document.getElementById('hasil').textContent =
'Gagal, coba lagi: ' + err.message;
btn.disabled = false;
})
.ambilNomor();
});
function muatStatus() {
google.script.run
.withSuccessHandler(function (s) {
document.getElementById('dipanggil').textContent = s.sedangDipanggil;
document.getElementById('menunggu').textContent = s.menunggu;
})
.getStatus();
}
muatStatus();
setInterval(muatStatus, 4000);
</script>
</body>
</html>Tombol sengaja di-disable selama pengiriman biar tidak dobel-klik jadi dua nomor. Dan jangan tergoda polling tiap setengah detik: tiap panggilan google.script.run itu satu eksekusi script, dan Google membatasi jumlah eksekusi harian serta eksekusi bersamaan. Untuk layar antrian, 3 sampai 5 detik sudah lebih dari cukup. Detail batasannya ada di batas kuota Apps Script. Kalau kamu juga mau menyimpan data lain dari form HP, polanya sama seperti di input data ke Spreadsheet dari HP.
Halaman petugas: panggil berikutnya
Petugas butuh satu tombol untuk maju ke nomor berikutnya. Fungsi panggilBerikutnya sudah ada di server, tinggal halaman yang memanggilnya. Simpan sebagai petugas.html. doGet tadi sudah mengarahkan ke file ini kalau URL dibuka dengan parameter token yang benar.
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Petugas</h1>
<p>Sedang dipanggil: <b id="dipanggil">-</b></p>
<p>Menunggu: <span id="menunggu">0</span></p>
<button id="btnPanggil">Panggil Berikutnya</button>
<script>
var btn = document.getElementById('btnPanggil');
function muatStatus() {
google.script.run
.withSuccessHandler(function (s) {
document.getElementById('dipanggil').textContent = s.sedangDipanggil;
document.getElementById('menunggu').textContent = s.menunggu;
})
.getStatus();
}
btn.addEventListener('click', function () {
btn.disabled = true;
google.script.run
.withSuccessHandler(function (res) {
document.getElementById('dipanggil').textContent = res.sedangDipanggil;
muatStatus();
btn.disabled = false;
})
.panggilBerikutnya();
});
muatStatus();
setInterval(muatStatus, 4000);
</script>
</body>
</html>Petugas cukup membuka URL Web App yang sama ditambah ?token=... di belakangnya. Ingat gerbang token ini sederhana, jadi pakai string acak yang panjang dan jangan sebar URL-nya. Kalau butuh keamanan sungguhan, tempuh jalur Web App terpisah berakses terbatas seperti di catatan gotcha tadi.
Deploy sebagai Web App
- Di editor Apps Script, klik Deploy, lalu New deployment.
- Pilih tipe Web app.
- Execute as: Me (akun kamu, biar script boleh menulis ke Spreadsheet-mu).
- Who has access: Anyone, supaya pengunjung tanpa login Google bisa buka.
- Klik Deploy, lalu setujui Authorize saat diminta.
- Salin URL web app. Itu link untuk pengunjung; petugas memakai URL yang sama plus ?token=TOKEN_PETUGAS.
Langkah lengkapnya, termasuk cara ambil ID Spreadsheet, ada di membuat Web App dari Spreadsheet. Satu jebakan umum: setiap kali kamu ubah kode, buat versi baru lewat Manage deployments, Edit, lalu New version. Kalau tidak, URL lama masih menyajikan versi kode yang lama.
Reset harian: sudah otomatis, bisa dipertegas
Reset harian sudah jalan sendiri lewat pengecekan tanggal di bacaState_. Tapi kalau kamu mau lebih tegas, misalnya mengarsipkan log kemarin ke sheet lain tepat tengah malam biar sheet Antrian tetap ramping, pasang trigger berbasis waktu. Caranya ada di menjadwalkan script dengan trigger.
Dari ide ke PRD, lalu ke agent
Perhatikan yang bikin app ini jalan bukan kodenya dulu, tapi keputusan strukturnya: sheet apa, kolom apa, fungsi server apa, dan batasan GAS apa yang harus disadari (lock saat menulis, kuota polling, deploy anonim tanpa identitas user). Itu persis isi sebuah PRD untuk agent seperti Cursor atau Claude Code. Baru soal PRD? Lihat apa itu PRD dan cara menjelaskan ide aplikasi ke AI.
Mode Web GAS di Skematik menuliskan semua itu secara terstruktur: skema tab Spreadsheet, daftar fungsi server, konvensi sadar-batasan GAS, sampai file setup-sheets.gs untuk membikin tab-nya sekali klik. Tinggal serahkan PRD-nya ke agent, dan kamu mulai dari kerangka yang benar, bukan dari nol.
Buat PRD Web GAS gratis di Skematik →Kalau kamu mau lihat contoh Web GAS yang sudah jadi dengan pola sama (Spreadsheet sebagai database, fungsi server, google.script.run), template KasirSheet bisa jadi titik mulai yang enak dibedah lalu dimodifikasi jadi versi antrianmu sendiri.
Lihat contoh PRD Web GAS (KasirSheet) →