{
  "id": "8qGkDG1Wvuk4Ds6K",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Screen resumes from Gmail with Gemini",
  "tags": [],
  "nodes": [
    {
      "id": "af8d5c1c-ef28-45da-820a-a77bebdbf0a5",
      "name": "Template Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -320,
        -16
      ],
      "parameters": {
        "color": 4,
        "width": 680,
        "height": 1064,
        "content": "## \ud83d\udcc4 Screen Resumes from Gmail with Gemini\n\n## Who's it for\nSmall businesses and recruiters who get job applications by email and want resumes automatically screened against the role's requirements, without spending time reading every single one, while keeping a human in the loop before anyone actually gets rejected.\n\n## How it works\nWhen an email with an attachment arrives, the workflow extracts the resume text and asks Gemini to screen it against your job title, required skills, and minimum experience. Every candidate is logged regardless of outcome. Shortlisted candidates trigger an email straight to the recruiter. Clear rejections get a polite reply saved as a Gmail draft, not sent automatically, so a human still approves it before it goes out. Anything ambiguous is labeled for manual review instead of being auto-decided either way.\n\n## How to set up\n1. Connect your Gmail OAuth2 credential to the trigger and both Gmail action nodes.\n2. Connect your Google Gemini API credential to the Chat Model node.\n3. Connect your Supabase credentials and create the table described in the sticky note near the Log Candidate node.\n4. In Gmail, create a label (e.g. \"Needs Manual Review\") and find its label ID.\n5. Open the Set Configuration node and fill in the job title, required skills, minimum experience, recruiter's email, and the label ID.\n6. Activate the workflow.\n\n## Requirements\n- A Gmail account with OAuth2 credentials in n8n\n- A Google Gemini API key\n- A Supabase project\n\n## How to customize\nEdit the screening prompt to change how strict the shortlist/reject line is, or to weigh certain skills more heavily. Edit the rejection draft's wording to match your company's tone."
      },
      "typeVersion": 1
    },
    {
      "id": "e3d3691a-8252-410f-ac2b-a34e608374fe",
      "name": "Section - Trigger and Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        368
      ],
      "parameters": {
        "color": 7,
        "width": 620,
        "height": 340,
        "content": "## 1\ufe0f\u20e3 2\ufe0f\u20e3 Trigger + Configuration\nFires on emails with attachments. Verify the filter query matches how applications actually arrive in your inbox (e.g. add a subject filter if you use a dedicated \"careers@\" address)."
      },
      "typeVersion": 1
    },
    {
      "id": "5b9953bb-16dd-4d8b-b26d-4c8f35a35199",
      "name": "Section - Extract and Screen",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1056,
        320
      ],
      "parameters": {
        "color": 7,
        "width": 872,
        "height": 608,
        "content": "## 3\ufe0f\u20e3 4\ufe0f\u20e3 Extract + Screen\nPulls text out of the attached resume, then Gemini compares it against your job requirements. \u26a0\ufe0f Verify the binary field name below matches what your Gmail attachments actually come through as (commonly attachment_0)."
      },
      "typeVersion": 1
    },
    {
      "id": "cfa8fabe-f530-4413-911f-6e0bc370b47b",
      "name": "Supabase Schema Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1584,
        736
      ],
      "parameters": {
        "color": 3,
        "width": 300,
        "content": "\u26a0\ufe0f Table schema\ncandidates: candidate_name, years_experience, matched_skills (jsonb), missing_skills (jsonb), decision, reason, created_at (timestamptz, default now())."
      },
      "typeVersion": 1
    },
    {
      "id": "55ac9e06-3d8e-49d4-a218-6a2ed5337533",
      "name": "Section - Shortlist Branch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2272,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 324,
        "content": "## 5\ufe0f\u20e3 If Shortlisted\nSends the recruiter a real email right away, this is internal and low-risk, so no draft-only caution needed here."
      },
      "typeVersion": 1
    },
    {
      "id": "d6f521d2-a2a4-413c-81e5-0228afba391f",
      "name": "Section - Reject Branch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2272,
        384
      ],
      "parameters": {
        "color": 7,
        "width": 444,
        "height": 412,
        "content": "## 6\ufe0f\u20e3 If Rejected\nSaves a polite rejection as a Gmail draft, never sent automatically. A human still reviews and hits send, same draft-only pattern used elsewhere: the AI shouldn't be the one deciding a real candidate gets turned away without a second look."
      },
      "typeVersion": 1
    },
    {
      "id": "b7625ded-ebbb-4702-a7c1-2d5a7f937026",
      "name": "Section - Review Branch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2272,
        848
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 448,
        "content": "## 7\ufe0f\u20e3 If Ambiguous\nNo automatic decision either way, just labeled so a person makes the final call."
      },
      "typeVersion": 1
    },
    {
      "id": "7ca9195b-2feb-4cc9-8b6c-ca1a557fd877",
      "name": "New Application Email",
      "type": "n8n-nodes-base.gmailTrigger",
      "position": [
        464,
        544
      ],
      "parameters": {
        "filters": {
          "q": "has:attachment"
        },
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        }
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "135e1c4f-e553-4de7-8ec8-8e96118d3fad",
      "name": "Set Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        768,
        544
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-1",
              "name": "job_title",
              "type": "string",
              "value": "REPLACE_WITH_JOB_TITLE"
            },
            {
              "id": "cfg-2",
              "name": "required_skills",
              "type": "string",
              "value": "REPLACE_WITH_COMMA_SEPARATED_SKILLS"
            },
            {
              "id": "cfg-3",
              "name": "min_experience_years",
              "type": "number",
              "value": 2
            },
            {
              "id": "cfg-4",
              "name": "recruiter_email",
              "type": "string",
              "value": "REPLACE_WITH_RECRUITER_EMAIL"
            },
            {
              "id": "cfg-5",
              "name": "supabase_table_candidates",
              "type": "string",
              "value": "candidates"
            },
            {
              "id": "cfg-6",
              "name": "needs_review_label_id",
              "type": "string",
              "value": "REPLACE_WITH_GMAIL_LABEL_ID"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "f249701c-a1e0-4720-a287-a1f4cb42b271",
      "name": "Extract Resume Text",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        1088,
        544
      ],
      "parameters": {
        "options": {},
        "operation": "pdf",
        "binaryPropertyName": "attachment_0"
      },
      "typeVersion": 1
    },
    {
      "id": "a16023d6-6c29-480e-9299-971a191d5a55",
      "name": "Screen Candidate",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        1328,
        544
      ],
      "parameters": {
        "text": "=You are screening a resume for this role.\n\nJob title: {{ $('Set Configuration').item.json.job_title }}\nRequired skills: {{ $('Set Configuration').item.json.required_skills }}\nMinimum experience: {{ $('Set Configuration').item.json.min_experience_years }} years\n\nResume text:\n{{ $json.text }}\n\nExtract the candidate's name and years of experience, list which required skills they clearly match and which are missing, and decide: \"shortlist\" if they clearly meet the requirements, \"reject\" if they clearly don't, or \"needs_human_review\" if it's genuinely unclear or borderline. Give a short reason for your decision.",
        "batching": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.8
    },
    {
      "id": "63c4cd8c-ff69-4f01-9039-04949f443d20",
      "name": "Screening Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        1248,
        768
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "22be2a37-0d96-43b4-86a2-b71537181b0a",
      "name": "Screening Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1424,
        784
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"candidate_name\": {\n      \"type\": \"string\"\n    },\n    \"years_experience\": {\n      \"type\": \"number\"\n    },\n    \"matched_skills\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"missing_skills\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"decision\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"shortlist\",\n        \"reject\",\n        \"needs_human_review\"\n      ]\n    },\n    \"reason\": {\n      \"type\": \"string\"\n    }\n  },\n  \"required\": [\n    \"candidate_name\",\n    \"years_experience\",\n    \"matched_skills\",\n    \"missing_skills\",\n    \"decision\",\n    \"reason\"\n  ]\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "d33e4a98-8a13-44d5-be0a-3d74705cce99",
      "name": "Log Candidate",
      "type": "n8n-nodes-base.supabase",
      "position": [
        1680,
        544
      ],
      "parameters": {
        "operation": "insert"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "a7df3d96-5846-459f-bd9e-3e8ddc532687",
      "name": "Route by Decision",
      "type": "n8n-nodes-base.switch",
      "position": [
        1952,
        512
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "b5fb4285-c8d7-4f5d-b963-7565195350de",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $('Screen Candidate').item.json.output.decision }}",
                    "rightValue": "shortlist"
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "fce356fb-bd48-4042-a14b-255c08e5755b",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $('Screen Candidate').item.json.output.decision }}",
                    "rightValue": "reject"
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "96d9db43-0d05-4cfb-81d9-5b381d2aafb4",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $('Screen Candidate').item.json.output.decision }}",
                    "rightValue": "needs_human_review"
                  }
                ]
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "3c8ff4dd-3487-418e-b153-57d25440b8bc",
      "name": "Notify Recruiter",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2368,
        208
      ],
      "parameters": {
        "sendTo": "={{ $('Set Configuration').item.json.recruiter_email }}",
        "message": "=Candidate: {{ $('Screen Candidate').item.json.output.candidate_name }}\nExperience: {{ $('Screen Candidate').item.json.output.years_experience }} years\nMatched skills: {{ $('Screen Candidate').item.json.output.matched_skills.join(', ') }}\nReason: {{ $('Screen Candidate').item.json.output.reason }}",
        "options": {},
        "subject": "=Shortlisted candidate: {{ $('Screen Candidate').item.json.output.candidate_name }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "22eca8d8-58ac-40e6-a4f2-e733ce0329a2",
      "name": "Save Rejection as Gmail Draft",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2400,
        608
      ],
      "parameters": {
        "message": "=Hi {{ $('Screen Candidate').item.json.output.candidate_name }},\n\nThank you for applying for the {{ $('Set Configuration').item.json.job_title }} role. After reviewing your application, we've decided to move forward with other candidates whose experience more closely matches what we're looking for right now.\n\nWe appreciate the time you took to apply and wish you the best in your search.\n\nBest regards",
        "options": {
          "threadId": "={{ $('New Application Email').item.json.threadId }}"
        },
        "subject": "=Re: Your application for {{ $('Set Configuration').item.json.job_title }}",
        "resource": "draft"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "d7482a1c-2e41-4de9-9586-2d026631ae23",
      "name": "Label Email as Needs Review",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2352,
        1040
      ],
      "parameters": {
        "labelIds": [
          "={{ $('Set Configuration').item.json.needs_review_label_id }}"
        ],
        "messageId": "={{ $('New Application Email').item.json.id }}",
        "operation": "addLabels"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "90531c62-4f39-4fab-a167-4381d87e2588",
  "nodeGroups": [],
  "connections": {
    "Log Candidate": {
      "main": [
        [
          {
            "node": "Route by Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Screen Candidate": {
      "main": [
        [
          {
            "node": "Log Candidate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Screening Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Screen Candidate",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Route by Decision": {
      "main": [
        [
          {
            "node": "Notify Recruiter",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save Rejection as Gmail Draft",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Label Email as Needs Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Configuration": {
      "main": [
        [
          {
            "node": "Extract Resume Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Resume Text": {
      "main": [
        [
          {
            "node": "Screen Candidate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Screening Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Screen Candidate",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "New Application Email": {
      "main": [
        [
          {
            "node": "Set Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}