Skip to content

Exercise 1

Replace the No Operation node with Gmail (or Slack):

  • Subject: {{ $json.needs_review ? '📄 Please review: ' + $json.original_name : '⚠️ Filing failed: ' + $json.original_name }}
  • Message:
{{ $json.needs_review ? 'Reason: ' + $json.review_reason : 'Error: ' + $json.upload_error }}
Best guess: {{ $json.type_label }} ({{ $json.confidence }}), {{ $json.fields.counterparty || 'unknown sender' }}
{{ $json.file_link ? 'File: ' + $json.file_link : '' }}

Exercise 2

New trigger branch:

  1. Gmail Trigger: poll every minute, Filters → Search to:docs@yourfirm.com has:attachment filename:pdf, Simplify off, Options → Download Attachments on.
  2. Code One Item per PDF:
    const out = [];
    for (const item of $input.all())
      for (const [key, file] of Object.entries(item.binary ?? {}))
        if (file.mimeType === 'application/pdf')
          out.push({ json: { from: item.json.from?.value?.[0]?.address ?? '', subject: item.json.subject }, binary: { file } });
    return out;
    
  3. Generalize the three places that read $('New Document'): the Has PDF? expression, the Extract From File binary field, and Attach File. Read from the current item's binary instead ($binary in expressions, $input.first().binary in Code), and carry the binary through Rule Classifier by returning binary: $input.first().binary there.

For several attachments per email, keep the per-item pairing ($input.all() + index) instead of .first() in the Code nodes. This solution hasn't been run against a live inbox yet.

Exercise 3

After Rule Classifier, add IF Scanned? ({{ $json.text_length < 20 }}):

  • true → OpenAI node: Resource Image → Analyze Image (model with vision, e.g. gpt-5-mini), Input Type Binary, binary from the upload. Prompt: "Transcribe all text in this document exactly." For multi-page PDFs, convert pages to images first (e.g. a small conversion service), since the analyze-image operation expects images.
  • → Code Use OCR Text: set text to the transcription and re-run the rule logic (or go straight to AI Classify).

Not run against a real scanned file; check the OpenAI node's output field for the transcription in your n8n version.

Exercise 4

After Upload Result (and on the dry-run branch if you want to log those too), Google Sheets → Append Row to Document Log:

Column Value
received_at {{ $now.toISO() }}
original_name {{ $json.original_name }}
filename {{ $json.filename }}
doc_type {{ $json.doc_type }}
confidence {{ $json.confidence }}
method {{ $json.method }}
counterparty {{ $json.fields.counterparty }}
amount {{ $json.fields.amount }}
folder {{ $json.folder_key }}
link {{ $json.file_link }}

Turn on Continue on Error for the Sheets node too, so a logging problem never blocks filing.