This n8n Google Sheets AI workflow scores new leads without turning the spreadsheet into a fragile database. It reads only unprocessed rows, validates required fields, calculates a transparent score, optionally normalizes messy job titles with AI, and updates the exact original row.

The important part is not the score. It is the discipline around stable IDs, data types, duplicate runs, and row updates. Those are the details most quick tutorials skip—and the details that prevent one lead from overwriting another.

← Return to the practical n8n workflow tutorials

What you will build

The core workflow uses six nodes:

  1. Schedule Trigger — starts every 15 minutes.
  2. Google Sheets — Get Rows — reads the lead table.
  3. If — Unprocessed? — allows only blank-status rows to continue.
  4. If — Required Fields? — separates valid rows from review rows.
  5. Edit Fields — Calculate Score — adds a deterministic score and reason.
  6. Google Sheets — Update Row — writes to the exact row using lead_id or row number.
n8n Google Sheets lead scoring workflow from schedule to update row
Read, validate, score, and update. A stable ID and status field make the workflow safe to run repeatedly.

Step 1: prepare the spreadsheet

Create a sheet named Leads with these exact headers in row 1:

Column Example Purpose
lead_id L-1001 Stable unique update key
email asha@example.com Required contact value
company Northstar Labs Lead context
employee_count 320 Numeric scoring input
country Singapore Regional scoring input
job_title Head of Ops Optional AI-normalization input
status blank Idempotency gate
score blank Workflow output
reason blank Human-readable output
processed_at blank Audit timestamp

Give every row a unique lead_id before n8n processes it. Email is not a reliable key: two rows can share an address, and a person can change addresses. Protect the header row and keep header spelling stable.

Recommended Google Sheets columns and example lead-scoring rows
A stable sheet contract makes expressions predictable. Invalid rows get a visible needs_review status instead of disappearing.

Step 2: connect Google Sheets safely

Create or select the Google Sheets credential in n8n. Use a dedicated automation account where possible and share only the required spreadsheet with that account. A lead-scoring workflow does not need access to every file in your Drive.

Create a workflow named Score new Google Sheets leads. Add Schedule Trigger and set it to every 15 minutes while testing. You can reduce the interval later if the use case really requires it.

Add Google Sheets and rename it Get Lead Rows:

  • Credential: your restricted Google Sheets credential
  • Resource: Sheet Within Document
  • Operation: Get Row(s)
  • Document: select or paste the target spreadsheet
  • Sheet: Leads

Execute the node. Confirm that each sheet row is a separate n8n item and that the output includes either a row number or your lead_id. Inspect employee_count: depending on the sheet and node settings, it may arrive as a Number or String. Do not assume.

Step 3: stop already-processed rows

Add an If node named Unprocessed?. Configure a String condition:

  • Value 1: {{ $json.status }}
  • Operator: is empty

The true output continues. The false output ends quietly. This makes the workflow safe to run again because rows marked ready or needs_review do not re-enter scoring.

Status alone is useful but not perfect locking. If overlapping executions are possible, prevent workflow concurrency or reserve the row before expensive AI calls. Otherwise two executions may both read the same blank status.

Step 4: validate required fields and types

Add a second If node named Required Fields?. The valid route requires:

  • lead_id is not empty;
  • email is not empty;
  • employee_count can be converted to a number;
  • country is not empty.

On the false route, add Edit Fields with:

status = needs_review
reason = Missing or invalid required field
processed_at = {{ $now }}

Connect that node to an Update Row node so invalid records are visible in the sheet rather than retried forever.

Step 5: calculate a transparent lead score

Add Edit Fields and rename it Calculate Score. Turn on Include Other Input Fields. Add a Number field named score with this expression:

{{
  (Number($json.employee_count) >= 200 ? 40 : 10) +
  (["Singapore", "India", "UAE"].includes($json.country) ? 30 : 10) +
  ($json.email ? 30 : 0)
}}

Add these String fields:

status = ready
reason = {{ Number($json.employee_count) >= 200 ? 'Large company; ' : 'Small company; ' }}{{ ["Singapore", "India", "UAE"].includes($json.country) ? 'target region' : 'other region' }}
processed_at = {{ $now }}

For the sample row with 320 employees, Singapore, and an email, the expected score is 100. A row with 45 employees, UAE, and an email scores 70. Calculate those results yourself before running the node.

This score is intentionally simple. Change the weights only when you have evidence that a field predicts value. A complicated formula does not automatically become a good model.

Optional: normalize job titles with AI

AI is useful when the raw title varies—Head Ops, Operations Lead, and VP, Business Operations—but you want one controlled category. It should not replace the deterministic score.

Between validation and Calculate Score, add a Basic LLM Chain with a Structured Output Parser. Prompt:

Classify this job title into exactly one allowed seniority:
executive, director, manager, individual_contributor, unknown.

Job title: {{ $json.job_title }}

If the title is ambiguous or empty, return unknown.

Require this schema:

{
  "type": "object",
  "properties": {
    "seniority": {
      "type": "string",
      "enum": ["executive", "director", "manager", "individual_contributor", "unknown"]
    },
    "confidence": {"type": "number", "minimum": 0, "maximum": 1}
  },
  "required": ["seniority", "confidence"]
}

Map the result back onto the original item and send confidence below 0.75 to needs_review. Do not let free-form model output become a sheet column or scoring rule.

Step 6: update the exact original row

Add Google Sheets and rename it Update Scored Lead:

  • Resource: Sheet Within Document
  • Operation: Update Row
  • Document/Sheet: the same spreadsheet and Leads sheet
  • Matching column: lead_id
  • Matching value: {{ $json.lead_id }}

Map status, score, reason, processed_at, and optional seniority. Preserve the original columns.

If your node version updates by row number, carry the row number from Get Lead Rows through every node. Do not update the “first row where email matches.” That is how duplicate email addresses corrupt the wrong record.

Step 7: create one run summary

Do not send one Slack message for every lead. Aggregate the run and report:

  • rows read;
  • rows skipped as already processed;
  • rows scored;
  • rows marked for review;
  • rows that failed to update.

For production, use an Error Trigger workflow so authentication errors, quota limits, and update failures produce an actionable alert with workflow ID, execution ID, and lead_id.

Test matrix

Input Expected result
Complete large-company Singapore row Score 100; status ready
Complete small-company UAE row Score 70; status ready
Missing email needs_review; no AI call
employee_count contains “three hundred” needs_review unless you explicitly support conversion
status already ready Skipped; no update
Duplicate email with different lead_id Only the matching lead_id row updates
Workflow run twice Second run skips completed rows
Google credential revoked Error workflow alerts; no partial silent success

Common problems

Problem Fix
Column is undefined Check the exact header text and actual Get Rows output
Score comparison is wrong Convert employee_count with Number() and validate it
Wrong row updated Match a unique lead_id or preserved row number
Same rows keep processing Verify status is written and the Unprocessed? condition uses the right field
Formula disappears Avoid mapping empty values over formula columns; update only intended fields
Quota or 429 errors Reduce polling, batch work, and add bounded backoff

A spreadsheet is convenient for human review, but it is not a high-concurrency database. When many workflows or users update the same data, move the state and locking into a real database and keep Sheets as a reporting or review surface.

Official references and next guides

Similar Posts