Cara Membuat Sistem Rekrutmen Online dengan Google Apps Script: Skor Kandidat Otomatis, Tahapan Seleksi, dan Jadwal Interview Anti Bentrok
Saat membuka lowongan, tim kecil biasanya memakai Google Form lalu membaca ratusan baris spreadsheet satu per satu. Lamaran yang jelas tidak memenuhi syarat tercampur dengan kandidat bagus, status pelamar dicatat dengan warna sel, dan jadwal interview bentrok karena dua orang HR mengundang kandidat di jam yang sama.
Di tutorial ini kita membangun inti sistem rekrutmen dengan Google Apps Script dan Google Sheets: lowongan yang menutup sendiri, pertanyaan knockout, skor kandidat otomatis, tahapan seleksi yang tertib, dan jadwal interview anti bentrok. Contohnya diambil dari aplikasi Rekrutmen Karyawan yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Lowongan yang menutup sendiri
Lowongan yang lupa ditutup adalah sumber masalah klasik: lamaran terus masuk padahal posisinya sudah terisi. Kita gabungkan tiga kondisi menjadi satu nilai buka saat membaca sheet:
var dl = teks_(r[8]), st = teks_(r[15]) === 'Buka' ? 'Buka' : 'Tutup';
var low = {
kode: teks_(r[0]).toUpperCase(),
deadline: dl,
kuota: Math.max(1, Number(r[9]) || 1),
statusSheet: st,
buka: st === 'Buka' && (!dl || dl >= hariIni_())
};
Status di sheet adalah keputusan manual HR, sedangkan deadline diperiksa setiap kali halaman dibuka. Kondisi ketiga, kuota, ditangani saat kandidat diterima (lihat langkah 4). Karena deadline disimpan sebagai teks yyyy-MM-dd dengan setNumberFormat('@'), perbandingan string dl >= hariIni_() sudah benar tanpa perlu objek Date.
2. Pertanyaan knockout yang ditulis di satu sel
Banyak posisi punya syarat mutlak: bersedia kerja di kantor, punya kendaraan, bisa magang enam bulan. Supaya HR bisa menulis sendiri tanpa menyentuh kode, kita pakai format sederhana di satu sel: Pertanyaan?|YA, dipisah titik koma. Bagian setelah | adalah jawaban wajib; kosong berarti bebas.
function parsePertanyaan_(s) {
return String(s || '').split(';').map(function (x) { return x.trim(); }).filter(String).slice(0, 5)
.map(function (x) {
var p = x.split('|');
return { tanya: p[0].trim(), wajib: (p[1] || '').trim().toUpperCase() };
});
}
// "Punya kendaraan sendiri?|YA;Bersedia shift akhir pekan?|"
Yang penting: ke halaman publik kita hanya mengirim teks pertanyaannya, bukan jawaban wajibnya.
pertanyaan: l.pertanyaan.map(function (q) { return q.tanya; })
Kalau jawaban wajib ikut terkirim, pelamar cukup membuka DevTools untuk tahu jawaban "yang benar", dan fungsi knockout jadi tidak berguna.
3. Skor kandidat dihitung di server
Dengan pertanyaan, pendidikan, dan pengalaman yang sudah terstruktur, kita bisa memberi skor 0-100 yang konsisten untuk setiap pelamar:
var PENDIDIKAN = ['SMA/SMK', 'D3', 'S1/D4', 'S2', 'S3'];
function hitungSkor_(low, pend, peng, jawaban) {
var gagal = low.pertanyaan.some(function (q, i) { return q.wajib && jawaban[i] !== q.wajib; });
if (gagal) return 0; // knockout
var s = 40, pi = PENDIDIKAN.indexOf(pend);
if (low.pendMin < 0 || pi >= low.pendMin)
s += 20 + Math.min(10, (pi - Math.max(0, low.pendMin)) * 5);
if (peng >= low.pengMin)
s += 20 + Math.min(10, Math.floor((peng - low.pengMin) * 2));
return Math.min(, s);
}
Pendidikan disimpan sebagai indeks dalam array berurutan, sehingga "S2 memenuhi syarat S1" cukup dihitung dengan >=. Bonus di atas syarat sengaja dibatasi (+10) supaya kandidat dengan pengalaman 15 tahun tidak otomatis mengalahkan kandidat yang sesuai kebutuhan. Skor ini hanya membantu urutan, bukan pengganti penilaian manusia; karena itu HR tetap punya rating bintang dan catatan sendiri.
Skor dihitung saat lamaran disimpan, di dalam LockService bersama pengecekan "satu email atau nomor WA hanya boleh melamar satu kali per posisi".
4. Tahapan seleksi yang hanya boleh maju
Papan kanban terasa bebas, tetapi data di belakangnya perlu aturan. Kandidat tidak boleh melompat dari Masuk langsung ke Diterima, dan kandidat final tidak boleh dipindah lagi:
var TAHAP = ['Masuk', 'Screening', 'Interview', 'Penawaran', 'Diterima'];
if (p.tahap === 'Diterima' || p.tahap === 'Ditolak') throw new Error('Lamaran ini sudah final.');
var dari = TAHAP.indexOf(p.tahap), ke = TAHAP.indexOf(tahap);
if (tahap !== 'Ditolak' && ke !== dari + 1) throw new Error('Pindahkan satu tahap demi satu tahap.');
if (tahap === 'Penawaran' && !p.jadwal) throw new Error('Isi jadwal interview dulu.');
if (tahap === 'Diterima') {
var terima = semua.filter(function (x) { return x.low === p.low && x.tahap === 'Diterima'; }).length;
if (terima >= l.kuota) throw new ();
(terima + >= l.) (l);
}
sh.(p., ).(p. + + tahap + + ());
Kolom Riwayat (Masuk@2026-10-01 09:12|Screening@2026-10-03 10:00|...) membuat laporan jadi mudah. Corong seleksi dihitung dari berapa kandidat yang pernah mencapai setiap tahap, dan time-to-hire adalah selisih hari antara Masuk dan Diterima.
5. Jadwal interview anti bentrok
Dua HR yang menjadwalkan interview dari HP masing-masing bisa saja memilih jam yang sama. Kita ubah jadwal menjadi menit sejak epoch lalu tolak jika jaraknya kurang dari 45 menit dari interview lain:
var mnt = function (j) { return new Date(j.slice(0, 16).replace(' ', 'T') + ':00+07:00').getTime() / 60000; };
var m = mnt(jadwal);
var tabrak = semua.filter(function (x) {
return x.id !== id && x.tahap === 'Interview' && x.jadwal && Math.abs(mnt(x.jadwal) - m) < 45;
})[0];
if (tabrak) throw new Error('Bentrok dengan interview ' + tabrak.nama + ' pukul ' + tabrak.jadwal.slice(11, 16) + '.');
Zona +07:00 ditulis eksplisit supaya hasilnya sama walaupun zona waktu server berbeda. Pengecekan dan penulisan berada di dalam satu kunci LockService, jadi dua penjadwalan bersamaan tidak bisa lolos keduanya.
6. Status untuk pelamar, data internal tetap internal
Pelamar bisa cek status dengan kode lamaran dan email. Fungsi ini sengaja membangun objek baru berisi field yang aman saja:
return {
id: p.id, nama: p.nama, posisi: l.posisi, tahap: p.tahap, urutan: TAHAP.indexOf(p.tahap),
jadwal: p.tahap === 'Interview' ? p.jadwal : ''
// tidak ada skor, rating, catatan, atau link CV
};
Lebih aman menulis daftar field yang boleh keluar (allowlist) daripada menghapus field yang tidak boleh (denylist). Kalau suatu hari kamu menambah kolom "Gaji Ditawarkan", kolom itu tidak akan bocor ke pelamar tanpa sengaja.
Ringkasan
- Gabungkan status manual, deadline, dan kuota menjadi satu nilai
buka. - Tulis pertanyaan knockout dalam format sederhana di sheet, tapi jangan kirim jawabannya ke browser.
- Hitung skor di server dari indeks pendidikan dan selisih pengalaman, dengan bonus yang dibatasi.
- Tahapan hanya boleh maju satu langkah; catat riwayat untuk corong dan time-to-hire.
- Cegah jadwal interview bentrok dengan jarak minimal di dalam LockService.
- Pakai allowlist saat mengirim data ke pelamar.
Download source code lengkapnya
Versi lengkapnya sudah mencakup halaman karier dengan filter, form lamaran dengan validasi link CV, papan kanban dengan filter skor, rating dan catatan HR, agenda interview, template WhatsApp undangan dan penolakan, laporan corong seleksi, sumber pelamar, serta ekspor CSV. Semua berjalan di Google Sheets tanpa hosting.
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.
Tutorial lainnya
Cara Membuat Sistem Tiket Travel dengan Pilih Kursi di Google Apps Script: Layout Armada, Anti Kursi Dobel, dan Kursi Kedaluwarsa
20 Oktober 2026
Cara Membuat Website Donasi Transparan dengan Google Apps Script: Kode Unik Transfer dan Laporan Penyaluran
20 Oktober 2026