This workflow follows the Gmail → Google Sheets recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Slip Gaji \u00b7 Pending only \u2192 PDF \u2192 Gmail (best practice)",
"nodes": [
{
"parameters": {
"content": "## Best practice slip gaji\n\n**Jangan** tarik semua lalu Limit (buang sisa).\n**Ya:** filter `periode` + `slip_status=pending` dulu.\n\nAlur:\n1. Config (periode, dry_run_email, max_per_run)\n2. Baca sheet\n3. Filter pending + periode\n4. Safety Limit (max_per_run)\n5. Batch 1 \u2192 PDF \u2192 Gmail\n6. Sukses \u2192 Update sent | Gagal \u2192 skip update\n7. Jeda \u2192 loop \u00b7 resume-safe\n\nCSV: `data/samples/karyawan-slip-gaji-1000.csv`\n",
"height": 420,
"width": 380,
"color": 4
},
"id": "sticky-guide",
"name": "Panduan best practice",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-520,
160
]
},
{
"parameters": {},
"id": "manual",
"name": "Mulai",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
0,
320
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "cfg-periode",
"name": "periode",
"value": "2026-08",
"type": "string"
},
{
"id": "cfg-dry",
"name": "dry_run_email",
"value": "egagofurtriwahana@gmail.com",
"type": "string"
},
{
"id": "cfg-max",
"name": "max_per_run",
"value": 5,
"type": "number"
}
]
},
"options": {}
},
"id": "config",
"name": "Config",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
220,
320
],
"notes": "periode = bulan gajian. dry_run_email = isi emailmu untuk tes (semua slip ke situ). Kosongkan dry_run_email untuk kirim ke email karyawan. max_per_run = sabuk pengaman per eksekusi."
},
{
"parameters": {
"operation": "read",
"documentId": {
"__rl": true,
"value": "PASTE_SHEET_DOCUMENT_ID",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "karyawan",
"mode": "name"
},
"options": {}
},
"id": "sheets-read",
"name": "Baca Data Karyawan",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
460,
320
],
"notes": "Idealnya filter di sumber (DB). Sheets: baca lalu Filter node. Untuk data sangat besar, pindah ke DB + WHERE pending."
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"combinator": "and",
"conditions": [
{
"id": "f-periode",
"leftValue": "={{ $json.periode }}",
"rightValue": "={{ $('Config').item.json.periode }}",
"operator": {
"type": "string",
"operation": "equals"
}
},
{
"id": "f-status",
"leftValue": "={{ $json.slip_status }}",
"rightValue": "pending",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"options": {}
},
"id": "filter-pending",
"name": "Filter pending + periode",
"type": "n8n-nodes-base.filter",
"typeVersion": 2.2,
"position": [
700,
320
],
"notes": "Hanya proses slip_status=pending dan periode = Config. Resume: yang sudah sent tidak diambil lagi."
},
{
"parameters": {
"maxItems": "={{ $('Config').item.json.max_per_run }}"
},
"id": "safety-limit",
"name": "Safety max per run",
"type": "n8n-nodes-base.limit",
"typeVersion": 1,
"position": [
940,
320
],
"notes": "Sabuk pengaman: max N per eksekusi (dari Config). Bukan pengganti filter pending. Run ulang = lanjut sisa pending."
},
{
"parameters": {
"batchSize": 1,
"options": {}
},
"id": "split-batch",
"name": "Satu per satu",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
1180,
320
],
"notes": "1 karyawan per loop. Loop kembali setelah update status."
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Mode: Run Once for Each Item\n// Slip gaji PDF \u2014 DOT style (hijau + putih, minimal, elegan)\n\nconst e = $json;\n\nfunction num(n) {\n return Number(n) || 0;\n}\nfunction idr(n) {\n return 'Rp ' + num(n).toLocaleString('id-ID');\n}\nfunction escPdf(s) {\n return String(s ?? '')\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\\(/g, '\\\\(')\n .replace(/\\)/g, '\\\\)');\n}\n\nconst gajiPokok = num(e.gaji_pokok);\nconst tTrans = num(e.tunjangan_transport);\nconst tMakan = num(e.tunjangan_makan);\nconst tJab = num(e.tunjangan_jabatan);\nconst lembur = num(e.lembur);\nconst bruto = num(e.bruto) || (gajiPokok + tTrans + tMakan + tJab + lembur);\nconst bpjsKes = num(e.potongan_bpjs_kesehatan);\nconst bpjsTk = num(e.potongan_bpjs_tk);\nconst pph = num(e.potongan_pph21);\nconst potLain = num(e.potongan_lain);\nconst totalPot = num(e.total_potongan) || (bpjsKes + bpjsTk + pph + potLain);\nconst neto = num(e.gaji_neto) || (bruto - totalPot);\nconst periode = e.periode || '2026-08';\nconst empId = e.employee_id || '-';\nconst nama = e.nama || '-';\nconst emailAsli = e.email || '';\nconst jabatan = e.jabatan || '-';\nconst dept = e.departemen || '-';\nconst bank = e.bank || '-';\nconst rek = e.no_rekening || '-';\n\nconst cfg = $('Config').first().json;\nconst emailTujuan = (cfg.dry_run_email && String(cfg.dry_run_email).trim())\n ? String(cfg.dry_run_email).trim()\n : emailAsli;\n\n// ---------- PDF DOT (A4, hijau + putih saja) ----------\nconst W = 595;\nconst H = 842;\nconst ML = 48;\nconst MR = 48;\nconst contentW = W - ML - MR;\n\nconst G = {\n 900: [0.07, 0.18, 0.11],\n 700: [0.1, 0.35, 0.2],\n 500: [0.18, 0.48, 0.3],\n 200: [0.86, 0.93, 0.88],\n 100: [0.94, 0.97, 0.95],\n 50: [0.98, 0.99, 0.98],\n};\nconst WHITE = [1, 1, 1];\n\nfunction rgb(a) {\n return a.map(function (x) {\n return Number(x.toFixed(3));\n }).join(' ');\n}\n\nconst ops = [];\nfunction push() {\n for (let i = 0; i < arguments.length; i++) ops.push(arguments[i]);\n}\nfunction fillRect(x, y, w, h, c) {\n push(rgb(c) + ' rg', x + ' ' + y + ' ' + w + ' ' + h + ' re', 'f');\n}\nfunction hLine(x1, y, x2, c, lw) {\n push(rgb(c) + ' RG', (lw || 0.5) + ' w', x1 + ' ' + y + ' m', x2 + ' ' + y + ' l', 'S');\n}\nfunction dot(cx, cy, r, c) {\n const k = 0.5522847498 * r;\n push(\n rgb(c) + ' rg',\n (cx + r) + ' ' + cy + ' m',\n (cx + r) + ' ' + (cy + k) + ' ' + (cx + k) + ' ' + (cy + r) + ' ' + cx + ' ' + (cy + r) + ' c',\n (cx - k) + ' ' + (cy + r) + ' ' + (cx - r) + ' ' + (cy + k) + ' ' + (cx - r) + ' ' + cy + ' c',\n (cx - r) + ' ' + (cy - k) + ' ' + (cx - k) + ' ' + (cy - r) + ' ' + cx + ' ' + (cy - r) + ' c',\n (cx + k) + ' ' + (cy - r) + ' ' + (cx + r) + ' ' + (cy - k) + ' ' + (cx + r) + ' ' + cy + ' c',\n 'f'\n );\n}\nfunction textAt(x, y, size, str, font, color) {\n push(\n 'BT',\n '/' + (font || 'F1') + ' ' + size + ' Tf',\n rgb(color || G[900]) + ' rg',\n '1 0 0 1 ' + x + ' ' + y + ' Tm',\n '(' + escPdf(str) + ') Tj',\n 'ET'\n );\n}\nfunction textRight(xRight, y, size, str, font, color) {\n const w = String(str).length * size * 0.5;\n textAt(xRight - w, y, size, str, font, color);\n}\n\n// page\nfillRect(0, 0, W, H, WHITE);\n// top bar\nfillRect(0, H - 6, W, 6, G[700]);\n\nlet y = H - 42;\ndot(ML + 3, y + 4, 2.2, G[700]);\ndot(ML + 11, y + 4, 2.2, G[700]);\ndot(ML + 19, y + 4, 2.2, G[500]);\ntextAt(ML + 30, y, 9, 'PT CONTOH TEKNOLOGI NUSANTARA', 'F2', G[700]);\ntextRight(W - MR, y, 8, 'SLIP GAJI', 'F1', G[500]);\n\ny = H - 68;\ntextAt(ML, y, 20, 'Slip Gaji', 'F2', G[900]);\ntextAt(ML, y - 16, 9, 'Periode ' + periode, 'F1', G[500]);\ntextRight(W - MR, y, 9, empId, 'F2', G[900]);\ntextRight(W - MR, y - 14, 8, 'ID Karyawan', 'F1', G[500]);\n\ny = H - 100;\nhLine(ML, y, W - MR, G[200], 0.7);\n\n// identity grid\ny -= 28;\nconst idTop = y;\nconst colL = ML;\nconst colR = ML + contentW * 0.52;\n\nfunction field(x, yy, label, value) {\n textAt(x, yy, 7.5, String(label).toUpperCase(), 'F1', G[500]);\n textAt(x, yy - 13, 10, value, 'F1', G[900]);\n}\n\nfield(colL, idTop, 'Nama', nama);\nfield(colR, idTop, 'Jabatan', jabatan);\nfield(colL, idTop - 36, 'Departemen', dept);\nfield(colR, idTop - 36, 'Email', emailAsli || '-');\nfield(colL, idTop - 72, 'Rekening', bank + ' ' + rek);\nfield(colR, idTop - 72, 'Periode', periode);\n\ny = idTop - 100;\nhLine(ML, y, W - MR, G[200], 0.7);\n\nfunction sectionTitle(yy, title) {\n dot(ML + 2.5, yy + 3, 2.4, G[700]);\n textAt(ML + 14, yy, 9, title.toUpperCase(), 'F2', G[700]);\n return yy - 8;\n}\n\nfunction tableBlock(startY, rows, totalLabel, totalValue) {\n const rowH = 18;\n const labelX = ML + 4;\n const amtR = W - MR - 4;\n let cy = startY;\n hLine(ML, cy, W - MR, G[200], 0.5);\n cy -= 4;\n\n rows.forEach(function (r, i) {\n cy -= rowH;\n if (i % 2 === 0) fillRect(ML, cy - 2, contentW, rowH, G[50]);\n textAt(labelX, cy + 4, 9, r.label, 'F1', G[900]);\n textRight(amtR, cy + 4, 9, r.value, 'F1', G[900]);\n });\n\n cy -= 6;\n hLine(ML, cy + rowH - 2, W - MR, G[200], 0.6);\n cy -= rowH;\n textAt(labelX, cy + 4, 9, totalLabel, 'F2', G[700]);\n textRight(amtR, cy + 4, 9, totalValue, 'F2', G[700]);\n hLine(ML, cy - 6, W - MR, G[700], 0.9);\n return cy - 14;\n}\n\ny = sectionTitle(y - 22, 'Pendapatan');\ny = tableBlock(\n y,\n [\n { label: 'Gaji pokok', value: idr(gajiPokok) },\n { label: 'Tunjangan transport', value: idr(tTrans) },\n { label: 'Tunjangan makan', value: idr(tMakan) },\n { label: 'Tunjangan jabatan', value: idr(tJab) },\n { label: 'Lembur', value: idr(lembur) },\n ],\n 'Total bruto',\n idr(bruto)\n);\n\ny = sectionTitle(y - 10, 'Potongan');\ny = tableBlock(\n y,\n [\n { label: 'BPJS Kesehatan', value: idr(bpjsKes) },\n { label: 'BPJS Ketenagakerjaan', value: idr(bpjsTk) },\n { label: 'PPh 21', value: idr(pph) },\n { label: 'Potongan lain', value: idr(potLain) },\n ],\n 'Total potongan',\n idr(totalPot)\n);\n\n// neto bar\ny -= 18;\nhLine(ML, y + 20, W - MR, G[200], 0.5);\nfillRect(ML, y - 8, contentW, 36, G[700]);\ntextAt(ML + 14, y + 10, 8, 'GAJI BERSIH (NETO)', 'F1', WHITE);\ntextAt(ML + 14, y - 2, 14, idr(neto), 'F2', WHITE);\ntextRight(W - MR - 14, y + 4, 8, periode, 'F1', G[200]);\n\n// footer\nconst footY = 40;\nhLine(ML, footY + 22, W - MR, G[200], 0.5);\ndot(ML + 2, footY + 8, 1.4, G[500]);\ndot(ML + 8, footY + 8, 1.4, G[500]);\ndot(ML + 14, footY + 8, 1.4, G[700]);\ntextAt(\n ML + 24,\n footY + 5,\n 7,\n 'Dokumen rahasia - digenerate otomatis - hubungi HR bila ada ketidaksesuaian',\n 'F1',\n G[500]\n);\ntextAt(ML + 24, footY - 8, 7, 'PT Contoh Teknologi Nusantara', 'F1', G[500]);\n\nconst contentStream = ops.join('\\n');\n\nconst parts = [];\nparts.push('1 0 obj<< /Type /Catalog /Pages 2 0 R >>endobj\\n');\nparts.push('2 0 obj<< /Type /Pages /Kids [3 0 R] /Count 1 >>endobj\\n');\nparts.push(\n '3 0 obj<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 4 0 R /Resources<< /Font<< /F1 5 0 R /F2 6 0 R >> >> >>endobj\\n'\n);\nparts.push(\n '4 0 obj<< /Length ' +\n Buffer.byteLength(contentStream, 'utf8') +\n ' >>stream\\n' +\n contentStream +\n '\\nendstream\\nendobj\\n'\n);\nparts.push('5 0 obj<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>endobj\\n');\nparts.push('6 0 obj<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>endobj\\n');\n\nlet pdf = '%PDF-1.4\\n';\nconst offsets = [0];\nfor (let i = 0; i < parts.length; i++) {\n offsets.push(Buffer.byteLength(pdf, 'utf8'));\n pdf += parts[i];\n}\nconst xrefPos = Buffer.byteLength(pdf, 'utf8');\nlet xref = 'xref\\n0 ' + (parts.length + 1) + '\\n';\nxref += '0000000000 65535 f \\n';\nfor (let i = 1; i <= parts.length; i++) {\n xref += String(offsets[i]).padStart(10, '0') + ' 00000 n \\n';\n}\npdf += xref;\npdf += 'trailer<< /Size ' + (parts.length + 1) + ' /Root 1 0 R >>\\n';\npdf += 'startxref\\n' + xrefPos + '\\n%%EOF';\n\nconst fileName = 'slip-gaji-' + empId + '-' + periode + '.pdf';\n\n// Email HTML \u2014 DOT, hijau + putih\nconst emailHtml =\n '<!DOCTYPE html><html><body style=\"margin:0;padding:0;background:#ffffff;font-family:Helvetica,Arial,sans-serif;color:#122E1C;\">' +\n '<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#ffffff;padding:32px 16px;\"><tr><td align=\"center\">' +\n '<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width:520px;border-top:4px solid #1A5933;\">' +\n '<tr><td style=\"padding:28px 0 8px;\">' +\n '<p style=\"margin:0 0 4px;font-size:11px;letter-spacing:0.08em;color:#2E7A4C;\">PT CONTOH TEKNOLOGI NUSANTARA</p>' +\n '<h1 style=\"margin:0;font-size:22px;font-weight:700;color:#122E1C;\">Slip Gaji</h1>' +\n '<p style=\"margin:8px 0 0;font-size:13px;color:#2E7A4C;\">Periode ' +\n periode +\n ' \u00b7 ' +\n empId +\n '</p>' +\n '</td></tr>' +\n '<tr><td style=\"padding:16px 0;border-top:1px solid #DBEDE1;border-bottom:1px solid #DBEDE1;\">' +\n '<p style=\"margin:0 0 12px;font-size:14px;line-height:1.5;\">Halo <strong>' +\n nama +\n '</strong>,</p>' +\n '<p style=\"margin:0;font-size:14px;line-height:1.5;color:#122E1C;\">Slip gaji periode <strong>' +\n periode +\n '</strong> terlampir dalam format PDF.</p>' +\n '</td></tr>' +\n '<tr><td style=\"padding:20px 0 8px;\">' +\n '<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-size:13px;\">' +\n '<tr><td style=\"padding:6px 0;color:#2E7A4C;\">Gaji bruto</td><td style=\"padding:6px 0;text-align:right;\">' +\n idr(bruto) +\n '</td></tr>' +\n '<tr><td style=\"padding:6px 0;color:#2E7A4C;\">Total potongan</td><td style=\"padding:6px 0;text-align:right;\">' +\n idr(totalPot) +\n '</td></tr>' +\n '<tr><td style=\"padding:12px 0 0;border-top:1px solid #DBEDE1;font-weight:700;\">Gaji bersih</td>' +\n '<td style=\"padding:12px 0 0;border-top:1px solid #DBEDE1;text-align:right;font-weight:700;color:#1A5933;\">' +\n idr(neto) +\n '</td></tr>' +\n '</table></td></tr>' +\n '<tr><td style=\"padding:24px 0 0;font-size:11px;color:#2E7A4C;line-height:1.5;\">' +\n 'Dokumen rahasia. Hubungi HR bila ada ketidaksesuaian.<br/>Email otomatis via n8n.' +\n '</td></tr>' +\n '</table></td></tr></table></body></html>';\n\nreturn {\n json: {\n employee_id: empId,\n nama: nama,\n email: emailTujuan,\n email_asli: emailAsli,\n jabatan: jabatan,\n departemen: dept,\n periode: periode,\n gaji_bruto: bruto,\n gaji_neto: neto,\n email_subject: 'Slip Gaji ' + periode + ' \u2014 ' + nama + ' (' + empId + ')',\n email_html: emailHtml,\n fileName: fileName,\n dry_run: !!(cfg.dry_run_email && String(cfg.dry_run_email).trim()),\n },\n binary: {\n slipPdf: {\n data: Buffer.from(pdf, 'utf8').toString('base64'),\n mimeType: 'application/pdf',\n fileName: fileName,\n fileExtension: 'pdf',\n },\n },\n};\n"
},
"id": "code-pdf",
"name": "Generate Slip PDF",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1440,
200
],
"notes": "PDF DOT style: hijau+putih, minimal, elegan. Run Once for Each Item."
},
{
"parameters": {
"sendTo": "={{ $json.email }}",
"subject": "={{ $json.email_subject }}",
"emailType": "html",
"message": "={{ $json.email_html }}",
"options": {
"attachmentsUi": {
"attachmentsBinary": [
{
"property": "slipPdf"
}
]
}
}
},
"id": "gmail-send",
"name": "Kirim Gmail + PDF",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
1700,
200
],
"notes": "Gmail OAuth2 + slipPdf. onError=continueErrorOutput: success\u2192Update sent, error\u2192Jeda (tetap pending). Dry-run: Config.dry_run_email.",
"onError": "continueErrorOutput"
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "PASTE_SHEET_DOCUMENT_ID",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "karyawan",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"employee_id": "={{ $('Generate Slip PDF').item.json.employee_id }}",
"slip_status": "sent",
"slip_sent_at": "={{ $now.setZone('Asia/Jakarta').toISO() }}"
},
"matchingColumns": [
"employee_id"
],
"schema": [
{
"id": "employee_id",
"displayName": "employee_id",
"required": false,
"defaultMatch": true,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "slip_status",
"displayName": "slip_status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": false
},
{
"id": "slip_sent_at",
"displayName": "slip_sent_at",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"id": "sheets-update-sent",
"name": "Update status sent",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
1960,
200
],
"notes": "employee_id dari Generate Slip PDF (bukan $json Gmail \u2014 response Gmail menimpa field). Match employee_id \u2192 slip_status=sent. Resume-safe."
},
{
"parameters": {
"amount": 2,
"unit": "seconds"
},
"id": "wait-rate",
"name": "Jeda 2s",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
2220,
200
],
"notes": "Rate limit Gmail. Loop balik ke batch."
},
{
"parameters": {
"amount": 2,
"unit": "seconds"
},
"id": "wait-on-error",
"name": "Jeda error 2s",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
1960,
420
],
"notes": "Gmail gagal \u2192 JANGAN update sent (tetap pending). Jeda lalu lanjut batch berikutnya."
},
{
"parameters": {
"content": "## Error path\n\nGmail gagal \u2192 **skip update sent** \u2192 jeda \u2192 loop batch.\nBaris tetap `pending` \u2192 run berikutnya coba lagi.\n\nJangan tandai `sent` sebelum email sukses.",
"height": 200,
"width": 300,
"color": 3
},
"id": "sticky-error",
"name": "Panduan error",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1700,
400
]
}
],
"connections": {
"Mulai": {
"main": [
[
{
"node": "Config",
"type": "main",
"index": 0
}
]
]
},
"Config": {
"main": [
[
{
"node": "Baca Data Karyawan",
"type": "main",
"index": 0
}
]
]
},
"Baca Data Karyawan": {
"main": [
[
{
"node": "Filter pending + periode",
"type": "main",
"index": 0
}
]
]
},
"Filter pending + periode": {
"main": [
[
{
"node": "Safety max per run",
"type": "main",
"index": 0
}
]
]
},
"Safety max per run": {
"main": [
[
{
"node": "Satu per satu",
"type": "main",
"index": 0
}
]
]
},
"Satu per satu": {
"main": [
[
{
"node": "Generate Slip PDF",
"type": "main",
"index": 0
}
],
[]
]
},
"Generate Slip PDF": {
"main": [
[
{
"node": "Kirim Gmail + PDF",
"type": "main",
"index": 0
}
]
]
},
"Kirim Gmail + PDF": {
"main": [
[
{
"node": "Update status sent",
"type": "main",
"index": 0
}
],
[
{
"node": "Jeda error 2s",
"type": "main",
"index": 0
}
]
]
},
"Update status sent": {
"main": [
[
{
"node": "Jeda 2s",
"type": "main",
"index": 0
}
]
]
},
"Jeda 2s": {
"main": [
[
{
"node": "Satu per satu",
"type": "main",
"index": 0
}
]
]
},
"Jeda error 2s": {
"main": [
[
{
"node": "Satu per satu",
"type": "main",
"index": 0
}
]
]
}
},
"meta": {
"templateCredsSetupCompleted": false
},
"settings": {
"executionOrder": "v1"
},
"tags": [
{
"name": "payroll"
},
{
"name": "best-practice"
},
{
"name": "gmail"
}
]
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Slip Gaji · Pending only → PDF → Gmail (best practice). Uses googleSheets, gmail. Event-driven trigger; 13 nodes.
Source: https://github.com/egagofur/n8n-boilerplate/blob/main/case-slip-gaji/workflow-slip-gaji.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow ingests proof-of-delivery and completion documents from Gmail or a webhook, extracts key fields with an OpenRouter vision model, reconciles them against Google Sheets dispatch data, arch
Loan eligibility workflow. Uses formTrigger, googleSheets, gmail. Event-driven trigger; 53 nodes.
Splitout Code. Uses manualTrigger, httpRequest, stickyNote, splitOut. Event-driven trigger; 46 nodes.
Automate CSV imports into HubSpot without the mess. Powered by n8n. Supercharged by Pollup AI.
Automate event registration with capacity management, a waitlist, and multi-tier PDF ticket generation using PDF Generator API. When attendees register, the workflow checks available spots, routes by