AutomationFlowsAI & RAG › Rank Resumes by Job Description Similarity Using Gemini Embeddings

Rank Resumes by Job Description Similarity Using Gemini Embeddings

ByAtha Ahsan Xavier Haris @athaahsan on n8n.io

This workflow collects resume PDFs and a job description via an n8n form, generates embeddings with Google Gemini, calculates cosine similarity between each resume and the job description, and returns a CSV-style ranked list of filenames. Receives resume PDF uploads and a job…

Event trigger★★★★☆ complexity11 nodesForm TriggerFormHTTP Request
AI & RAG Trigger: Event Nodes: 11 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #16720 — we link there as the canonical source.

This workflow follows the Form → Form Trigger 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 →

Download .json
{
  "id": "Ltr0qlNZ6EyR12Tl",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Rank resumes by job description similarity with Gemini embeddings",
  "tags": [],
  "nodes": [
    {
      "id": "5198c266-d677-42d7-8cc5-da8879415644",
      "name": "Resume Upload Form",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -128,
        -80
      ],
      "parameters": {
        "options": {
          "buttonLabel": "Sort Resumes"
        },
        "formTitle": "Sort Resumes by Job Description Similarity",
        "formFields": {
          "values": [
            {
              "fieldType": "file",
              "fieldLabel": "Resumes",
              "requiredField": true,
              "acceptFileTypes": ".pdf"
            },
            {
              "fieldType": "textarea",
              "fieldLabel": "Job Description",
              "requiredField": true
            }
          ]
        },
        "responseMode": "lastNode",
        "formDescription": "Upload resume PDFs and paste the job description. The workflow will return a CSV-formatted ranking of resumes by semantic similarity."
      },
      "typeVersion": 2.6
    },
    {
      "id": "2fe21d29-6afa-4641-aea5-0c64780d6378",
      "name": "Split Resume PDFs",
      "type": "n8n-nodes-base.code",
      "position": [
        128,
        -80
      ],
      "parameters": {
        "jsCode": "const formItem = $input.first();\n\nconst binary = formItem.binary || {};\n\nconst resumeKeys = Object.keys(binary).filter((key) =>\n\tkey === 'Resumes' ||\n\tkey.startsWith('Resumes_') ||\n\tkey.startsWith('Resumes.')\n);\n\nif (resumeKeys.length === 0) {\n\tthrow new Error('No resume PDFs were uploaded.');\n}\n\nreturn resumeKeys.map((key) => {\n\tconst file = binary[key];\n\n\tconst index = Number(key.replace('Resumes_', ''));\n\n\treturn {\n\t\tjson: {\n\t\t\tfileName: formItem.json.Resumes[index].filename,\n\t\t},\n\t\tbinary: {\n\t\t\tresume: file,\n\t\t},\n\t};\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "29f370bc-0c18-40c1-ac7d-7b3bf3cc4c5d",
      "name": "Show Results",
      "type": "n8n-nodes-base.form",
      "position": [
        1488,
        -80
      ],
      "parameters": {
        "options": {},
        "operation": "completion",
        "completionTitle": "Resume Ranking Complete",
        "completionMessage": "={{\n[\n  'Rank,File Name',\n  ...$input.all().map(item =>\n    `${item.json.rank},\"${item.json.fileName}\"`\n  )\n].join('\\n')\n}}"
      },
      "typeVersion": 2.5
    },
    {
      "id": "c7e52a97-87b6-4413-a8e8-868a81bea511",
      "name": "Convert PDF to Base64",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        592,
        -80
      ],
      "parameters": {
        "options": {},
        "operation": "binaryToPropery",
        "destinationKey": "base64",
        "binaryPropertyName": "resume"
      },
      "typeVersion": 1.1
    },
    {
      "id": "c6495fcc-0d28-4f2b-93b6-bb38024fdb64",
      "name": "Rank Resumes by Similarity",
      "type": "n8n-nodes-base.code",
      "position": [
        1248,
        -80
      ],
      "parameters": {
        "jsCode": "// 1. Grab the Job Description vector\nconst jdEmbedding = $('Create Job Description Embedding').first().json.embedding.values;\n\n// 2. Define the Cosine Similarity math engine\nfunction cosineSimilarity(a, b) {\n\tlet dot = 0;\n\tlet normA = 0;\n\tlet normB = 0;\n\n\tfor (let i = 0; i < a.length; i++) {\n\t\tdot += a[i] * b[i];\n\t\tnormA += a[i] * a[i];\n\t\tnormB += b[i] * b[i];\n\t}\n\n\treturn dot / (Math.sqrt(normA) * Math.sqrt(normB));\n}\n\n// 3. Map through the parallel resume items and calculate their scores\nconst rankedItems = $input.all().map((item, index) => {\n\tconst resumeEmbedding = item.json.embedding.values;\n\tconst fileName = $('Split Resume PDFs').all()[index].binary.resume.fileName;\n\n\treturn {\n\t\tjson: {\n\t\t\tfileName,\n\t\t\tscore: cosineSimilarity(jdEmbedding, resumeEmbedding),\n\t\t},\n\t};\n});\n\n// 4. Sort the array from highest similarity score to lowest\nrankedItems.sort((a, b) => b.json.score - a.json.score);\n\n// 5. Output the cleanly indexed final rankings\nreturn rankedItems.map((item, index) => ({\n\tjson: {\n\t\trank: index + 1,\n\t\tfileName: item.json.fileName,\n\t\tscore: item.json.score,\n\t},\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "60b79173-c0d5-4e25-9c05-2a8113802beb",
      "name": "Create Job Description Embedding",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        128,
        -304
      ],
      "parameters": {
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-2:embedContent",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"model\": \"models/gemini-embedding-2\",\n  \"content\": {\n    \"parts\": [\n      {\n        \"text\": {{ JSON.stringify($('Resume Upload Form').item.json['Job Description']) }}\n      }\n    ]\n  }\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googlePalmApi"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "executeOnce": false,
      "typeVersion": 4.4
    },
    {
      "id": "0252844a-930b-4197-91aa-404aa36c811c",
      "name": "Create Resume Embedding",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        848,
        -80
      ],
      "parameters": {
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-2:embedContent",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"content\": {\n    \"parts\": [\n      {\n        \"inline_data\": {\n          \"mime_type\": \"application/pdf\",\n          \"data\": \"{{ $json.base64 }}\"\n        }\n      }\n    ]\n  }\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googlePalmApi"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "48b9283e-b0f9-4985-93fa-edc9bb89f6b6",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1024,
        -816
      ],
      "parameters": {
        "width": 768,
        "height": 1376,
        "content": "## **Rank resumes by job description similarity with Gemini embeddings**\n\nThis workflow sorts uploaded resume PDFs by semantic similarity to a Job Description using Google Gemini multimodal embeddings.\n\nUnlike text-extraction workflows, this version does not extract resume text first and does not use a separate OCR branch. It converts each uploaded PDF into base64, sends the PDF content directly to Gemini Embedding 2, compares each resume vector against the Job Description vector with cosine similarity, and returns a ranked list of filenames.\n\nIt is useful for recruiters, HR teams, and hiring teams who want a simple PDF-to-vector resume sorter before manual review.\n\n## **Common use cases**\n\n- Sort resume PDFs against one Job Description\n- Compare text-based and scanned/image-based PDFs with one simpler flow\n- Prioritize manual resume review\n- Build a lightweight Gemini-native resume sorting workflow\n- Avoid LLM-based candidate scoring or explanation generation\n\n## **How it works**\n\n- User uploads resume PDFs and pastes a Job Description\n- The Job Description is embedded with `gemini-embedding-2`\n- Each resume PDF is converted into base64\n- Each PDF is sent directly to Gemini multimodal embeddings\n- Cosine similarity compares each resume vector to the Job Description vector\n- The workflow returns filenames sorted by similarity\n\n## **Setup steps**\n\n- Get a free Gemini API key from [Google AI Studio](https://aistudio.google.com/api-keys)\n- Create or connect a Google Gemini credential in n8n\n- Use that credential on:\n  - **Create Job Description Embedding**\n  - **Create Resume Embedding**\n- Make sure your Google account/API key can use `gemini-embedding-2`\n- Upload one or more resume PDFs\n- Paste the Job Description\n- Run the workflow\n- Review the sorted filename list\n\n## **Notes**\n\nThis workflow is a sorter, not an automated hiring decision system. It does not accept, reject, explain, or score candidates for the user. Similarity values are used internally for sorting.\n\nThe final output shows only:\n- Rank\n- File Name\n\n## **Need Help?**\nHave questions or want to connect? Reach me on [LinkedIn](https://www.linkedin.com/in/athaahsan/)."
      },
      "typeVersion": 1
    },
    {
      "id": "a87d8aac-a300-4ebd-8fa2-5d5d420fe5b3",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -192,
        -816
      ],
      "parameters": {
        "width": 624,
        "height": 928,
        "content": "## 1. Upload resumes and embed the Job Description\n\nThe form collects:\n- One or more resume PDFs\n- One Job Description text\n\nOnly one credential is required for this workflow: **Google Gemini**.\n\nYou can get a free Gemini API key from [Google AI Studio](https://aistudio.google.com/api-keys).\n\nSet the Google Gemini credential on:\n- **Create Job Description Embedding**\n- **Create Resume Embedding**\n\nThe Job Description is embedded once using `gemini-embedding-2`.\n\nThe Split Resume PDFs node turns uploaded files into separate resume items while preserving the original filename."
      },
      "typeVersion": 1
    },
    {
      "id": "ce268823-702e-42ee-94d8-f88f31675829",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        464,
        -512
      ],
      "parameters": {
        "width": 608,
        "height": 624,
        "content": "## 2. Convert each PDF directly into an embedding\n\nEach uploaded resume PDF is converted into a base64 string.\n\nThe base64 PDF content is then sent directly to Gemini Embedding 2 as `inline_data` with MIME type `application/pdf`.\n\nThis version does not use:\n- PDF text extraction\n- OCR branching\n- resume summarization\n- LLM candidate review\n\nGemini creates one embedding vector for each resume PDF."
      },
      "typeVersion": 1
    },
    {
      "id": "0702022b-1ced-4198-9a91-0fdcc429aa75",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1104,
        -512
      ],
      "parameters": {
        "width": 624,
        "height": 624,
        "content": "## 3. Compare vectors and return sorted filenames\n\nThe Code node compares:\n- Job Description embedding\n- Each resume PDF embedding\n\nIt uses cosine similarity to sort resumes from most similar to least similar.\n\nThe final result only shows:\n- Rank\n- File Name\n\nThe similarity score is used internally for sorting and is not shown in the final form response."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "c6745b78-6163-4b06-af86-bd08e6a18e43",
  "nodeGroups": [],
  "connections": {
    "Split Resume PDFs": {
      "main": [
        [
          {
            "node": "Convert PDF to Base64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resume Upload Form": {
      "main": [
        [
          {
            "node": "Split Resume PDFs",
            "type": "main",
            "index": 0
          },
          {
            "node": "Create Job Description Embedding",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert PDF to Base64": {
      "main": [
        [
          {
            "node": "Create Resume Embedding",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Resume Embedding": {
      "main": [
        [
          {
            "node": "Rank Resumes by Similarity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rank Resumes by Similarity": {
      "main": [
        [
          {
            "node": "Show Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Job Description Embedding": {
      "main": [
        []
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow collects resume PDFs and a job description via an n8n form, generates embeddings with Google Gemini, calculates cosine similarity between each resume and the job description, and returns a CSV-style ranked list of filenames. Receives resume PDF uploads and a job…

Source: https://n8n.io/workflows/16720/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

This workflow collects resume PDFs and a job description through an n8n form, extracts text (with OCR for scanned PDFs), generates embeddings via OpenRouter, and ranks the resumes by cosine similarity

Form Trigger, HTTP Request, Form
AI & RAG

AI-powered workflow that transforms any article URL into platform-optimized social media posts for LinkedIn, Twitter (X), and Reddit. Uses Mozilla Readability for content extraction, multi-agent AI wi

OpenAI Chat, Supabase Vector Store, OpenAI Embeddings +8
AI & RAG

Inquiry-Agent. Uses @digitalocean/n8n-nodes-digitalocean-gradient-serverless-inference, stopAndError, googleDocs, gmail. Event-driven trigger; 88 nodes.

@Digitalocean/N8N Nodes Digitalocean Gradient Serverless Inference, Stop And Error, Google Docs +12
AI & RAG

Inquiry-Agent. Uses @digitalocean/n8n-nodes-digitalocean-gradient-serverless-inference, stopAndError, googleDocs, gmail. Event-driven trigger; 80 nodes.

@Digitalocean/N8N Nodes Digitalocean Gradient Serverless Inference, Stop And Error, Google Docs +11
AI & RAG

Inquiry-Agent. Uses @digitalocean/n8n-nodes-digitalocean-gradient-serverless-inference, stopAndError, googleDocs, gmail. Event-driven trigger; 77 nodes.

@Digitalocean/N8N Nodes Digitalocean Gradient Serverless Inference, Stop And Error, Google Docs +10