Document Intake and Routing
Module 17 · business · intermediate
decide.js
// Combine rules (+ AI when it ran) into one routing decision: type, fields, new filename, target folder.
const s = $('Settings (edit me)').first().json;
const req = $('New Document').first().json;
const r = $('Rule Classifier').first().json.rules;
// AI Classify runs with "On Error: Continue": if it fails (no key, outage), fall back to the rules.
const aiRan = $('AI Classify').isExecuted;
const aiItem = aiRan ? $input.first().json : null;
const ai = aiItem && !aiItem.error && aiItem.output ? aiItem.output : null;
const aiError = aiItem && !ai ? (typeof aiItem.error === 'string' ? aiItem.error : aiItem.error?.message ?? 'AI step returned no output (check the OpenAI credential)') : null;
const LABELS = { invoice: 'Invoice', receipt: 'Receipt', contract: 'Contract', bank_statement: 'Bank Statement', purchase_order: 'Purchase Order', tax_form: 'Tax Form', other: 'Document' };
const pickType = (t) => (LABELS[t] ? t : 'other');
const docType = ai ? pickType(String(ai.doc_type ?? '').toLowerCase()) : r.doc_type;
const confidence = ai ? Math.max(0, Math.min(1, Number(ai.confidence) || 0)) : r.confidence;
const fields = {
document_date: (ai && ai.document_date) || r.fields.document_date,
counterparty: (ai && ai.counterparty) || r.fields.counterparty,
reference: (ai && ai.reference) || r.fields.reference,
amount: ai && ai.amount != null && ai.amount !== '' ? Number(ai.amount) : r.fields.amount,
};
const textEmpty = $('Rule Classifier').first().json.text_length < 20;
const needsReview = textEmpty || docType === 'other' || confidence < s.review_threshold;
const reason = textEmpty ? 'no text layer (scanned?)' : docType === 'other' ? 'unknown document type' : confidence < s.review_threshold ? `low confidence (${confidence})` : null;
// "2026-09-15 - Invoice - Northwind Office Supply LLC - NW-55821.pdf"
const clean = (x) => String(x ?? '').replace(/[\\/:*?"<>|\n\r]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 60);
const originalName = Object.values($('New Document').first().binary ?? {})[0]?.fileName ?? 'document.pdf';
const filename = needsReview
? `REVIEW - ${clean(originalName.replace(/\.pdf$/i, ''))}.pdf`
: [fields.document_date ?? DateTime.now().toISODate(), LABELS[docType], clean(fields.counterparty), clean(fields.reference)].filter(Boolean).join(' - ') + '.pdf';
const folderKey = needsReview ? 'needs_review' : docType;
return [{
json: {
doc_type: docType,
type_label: LABELS[docType],
confidence,
method: ai ? 'ai' : 'rules',
ai_error: aiError,
needs_review: needsReview,
review_reason: reason,
fields,
summary: ai?.summary ?? null,
filename,
folder_key: folderKey,
folder_id: s.folders[folderKey],
original_name: originalName,
dry_run: req.query?.dry_run === 'true',
},
}];