Post-Interview Debriefs: 5 Days to 5 Hours With 1 n8n Agent

Abstract tech illustration: Post-Interview Debriefs: 5 Days to 5 Hours With 1 n8n Agent

Every hiring automation demo stops when the interview ends. Then four scorecards sit in Google Docs for a week, your hiring manager reads them on Sunday, and your best candidate signs somewhere else on Monday. That gap — last interview to signed offer — is where small teams lose senior hires, and it's the one step nobody automates.

I built this for a 12-person staffing agency. Their time-to-offer on senior roles was 5.2 days. After we shipped a four-node n8n workflow, it dropped to 1.4 days across the next 38 hires. Here's the actual system.

Why averaging four scorecards is how you hire the wrong person

Your ATS has a scorecard field. It's a text box. Greenhouse and Lever will average a 1–5 rating for you and stop there. Averaging a panel is exactly how you hire someone who is fine at everything and great at nothing, while rejecting the specialist you actually needed.

Consider a senior backend role where four interviewers score on six competencies:

Competency I1 I2 I3 I4 Avg
Technical depth 5 5 2 5 4.25
System design 4 5 3 5 4.25
Communication 2 3 3 4 3.00
Team leadership 2 2 3 3 2.50
Product judgment 3 3 3 3 3.00
Ownership 4 4 3 4 3.75

Straight average is 3.46. Looks like a maybe. But interviewer 3 gave technical depth a 2 while three others gave it a 5. That's not noise — that's a signal to dig in. Averaging buries it. Weighted scoring plus a disagreement flag surfaces it. That's the whole gap.

The other thing averaging kills: role fit. A backend hire and a customer success hire don't share weights. Technical depth is worth 3× on backend, 0× on CS. Stakeholder communication is the opposite. One formula for all roles is worse than no formula.

The four-node n8n workflow, end to end

Four nodes, one Google Sheet for role weights, one Slack channel. No custom code beyond a single function node. Here's the shape:

[Google Drive Trigger]  →  [OpenAI Extract (JSON schema)]  →  [Function: weight + disagreement]  →  [OpenAI Brief + Slack post + Drive save]

Each candidate has one Drive folder. Four interviewers drop their scorecard docs into it. The workflow fires when either condition is met:

  • The fourth doc lands in the folder, OR
  • 48 hours have passed since the last scheduled interview

The second trigger is the one everyone forgets. If you wait forever on the interviewer who never writes their notes, you're back to 5 days. Better to run the brief with three scorecards and flag "waiting on I4" than to run it never.

What each node does

  • Node 1 — Drive watcher: polls the candidate folder every 5 min; fires on file count = 4 or a Cron branch that checks candidates whose last interview was >48h ago.
  • Node 2 — Extraction: one OpenAI call per doc, locked to a JSON schema. No freeform summaries.
  • Node 3 — Weighting function: pulls the role's row from the weights sheet, multiplies each per-competency score by its weight, computes panel score and per-competency disagreement.
  • Node 4 — Decision brief: second OpenAI call with structured input only. Posts to Slack, saves a PDF back to the candidate folder.

Node 2: why the JSON schema is non-negotiable

If you let the model summarize freely, it will smooth over disagreement. It will write "the panel was broadly positive with some reservations" when what actually happened is interviewer 1 said strong hire and interviewer 3 said no hire on the same competency. You want the disagreement preserved. That's the whole point of the panel.

Lock the schema. Here's the exact one we use:

{
  "type": "object",
  "properties": {
    "interviewer_name": { "type": "string" },
    "overall_recommendation": {
      "type": "string",
      "enum": ["strong_hire", "hire", "no_hire", "strong_no_hire"]
    },
    "competency_scores": {
      "type": "object",
      "properties": {
        "technical_depth":       { "type": "integer", "minimum": 1, "maximum": 5 },
        "system_design":         { "type": "integer", "minimum": 1, "maximum": 5 },
        "communication":         { "type": "integer", "minimum": 1, "maximum": 5 },
        "team_leadership":       { "type": "integer", "minimum": 1, "maximum": 5 },
        "product_judgment":      { "type": "integer", "minimum": 1, "maximum": 5 },
        "ownership":             { "type": "integer", "minimum": 1, "maximum": 5 }
      },
      "required": ["technical_depth","system_design","communication","team_leadership","product_judgment","ownership"]
    },
    "evidence_quotes": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Verbatim quotes from the doc supporting positive signal."
    },
    "concerns": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Verbatim quotes from the doc supporting concerns."
    }
  },
  "required": ["interviewer_name","overall_recommendation","competency_scores","evidence_quotes","concerns"]
}

Two things to notice:

  1. Verbatim quotes, not paraphrases. If the model paraphrases, "strong pushback on the caching approach" becomes "some technical concerns" and you lose the signal.
  2. Enums on the recommendation. No "leaning hire" or "hire with reservations." Four buckets, force the choice.

Use OpenAI's structured outputs (response_format: { type: "json_schema", strict: true }) so the call fails loud instead of returning malformed JSON that breaks node 3. On GPT-4-class models this runs at roughly $0.008–$0.015 per scorecard. Four scorecards = under 6 cents.

Node 3: role weights and the disagreement flag

The weights sheet is one row per role, one column per competency, values 0–3. Example:

Role Tech depth System design Comm Team lead Product judg Ownership
Senior backend eng 3 3 1 1 1 2
Staff backend eng 3 3 2 2 2 3
Customer success lead 0 0 3 2 3 2
Sales engineer 2 2 3 1 3 2

The n8n Function node does two things. First, the weighted panel score:

const weights = $node["Get Role Weights"].json;   // { technical_depth: 3, ... }
const scorecards = $node["Extract Scorecards"].json.all;  // array of 3-4 objects

const competencies = Object.keys(weights);
const panelAvg = {};
const disagreement = {};

for (const c of competencies) {
  const scores = scorecards.map(s => s.competency_scores[c]);
  const mean = scores.reduce((a,b) => a+b, 0) / scores.length;
  panelAvg[c] = mean;
  disagreement[c] = Math.max(...scores) - Math.min(...scores);  // spread
}

const weightedScore = competencies.reduce(
  (acc, c) => acc + (panelAvg[c] * weights[c]), 0
) / competencies.reduce((acc, c) => acc + weights[c], 0);

const splitVotes = competencies
  .filter(c => disagreement[c] >= 2)
  .map(c => ({
    competency: c,
    spread: disagreement[c],
    scores: scorecards.map(s => ({ name: s.interviewer_name, score: s.competency_scores[c] }))
  }));

return { weightedScore: weightedScore.toFixed(2), splitVotes, panelAvg };

A spread ≥ 2 on any weighted competency is the disagreement flag. That's what feeds into the brief as "split vote to resolve." Empirically, ~30% of candidates trigger at least one split-vote flag, and roughly half of those flags change the recommended next step from "make offer" to "one targeted follow-up call."

Node 4: the one-page brief that hiring managers actually read

The second OpenAI call takes the structured JSON from node 2 plus the weighted output from node 3, and produces a brief with exactly five sections:

  1. Recommendation — one word: hire / no hire / offer with follow-up
  2. Top 3 strengths with the verbatim quotes that support them
  3. Top 3 concerns with verbatim quotes
  4. Split votes — called out by interviewer name and competency
  5. Recommended next step — make offer, reject, or one targeted follow-up call

Posts to a private Slack channel, saves as PDF to the candidate's Drive folder. Here's a redacted brief from a recent senior data engineer:

Recommendation: Hire (weighted panel score 4.1 / 5.0, role: senior data eng)

Strengths

  • Deep streaming-pipeline experience — "described a Kafka → Iceberg backfill approach I hadn't seen before" (I2)
  • Strong system design — "cleared the round with a novel approach to schema evolution" (I4)
  • Ownership signal — "volunteered ownership of on-call rotation redesign at last role" (I2)

Concerns

  • Thin team-leadership signal — "never mentored anyone directly" (I1)
  • Hesitant on ambiguous product requirements — "stalled when I removed the acceptance criteria" (I3)

Split vote flagged

  • Communication: I1 = 2, I4 = 4 (spread 2)

Recommended next step: 20-minute call with engineering director to probe leadership readiness and resolve the communication split before offer.

The hiring manager read that in four minutes and made the call the same afternoon. Candidate signed 36 hours after the last interview. Two weeks earlier they'd lost a stronger candidate on the same role because the brief sat unwritten for six days.

Numbers, cost, and what actually broke

Across 38 hires on the same agency, after 90 days running this workflow:

  • Time-to-offer: 5.2 days → 1.4 days
  • Panel debrief writing time reclaimed per candidate: ~80 min → ~15 min (interviewers still write short scorecards; they just don't sync)
  • Senior close rate on offers extended: +22% (fewer competing offers on the table when ours lands)
  • API cost per candidate brief: $0.06–$0.11 end to end
  • Workflow build time in n8n: about 6 hours including the weights sheet template

Things that broke on the way:

  • Docs with tables. The Drive-to-text step munged tabular scorecards. Fix: force interviewers into a plain markdown template with H2 sections per competency.
  • Interviewer name mismatch. The model would return "Sarah" one week and "Sarah K." the next. Fix: extract from the doc's owner field via the Drive API, not from doc contents.
  • Split-vote noise on low-weight competencies. A spread of 2 on a competency weighted 0 shouldn't fire. Fix: only flag splits where weight ≥ 1.
  • The 48-hour timer firing during holidays. Fix: gate the timer branch on business days via a small date-check node.

Where bizflowai.io fits in

At bizflowai.io we build these post-interview and post-meeting decision-loop agents for small agencies and in-house recruiting teams — the kind where four people interview one candidate and no single tool owns the handoff. The pattern is the same across domains: structured extraction with a locked schema, a role-specific weighting layer, and a one-page brief that lands in the channel where the decision actually gets made. It's the last-mile automation most ATS integrations skip.


Want more like this?

I publish practical AI automation, GenAI engineering, and faceless content workflows on YouTube every week.

Subscribe to bizflowai.io on YouTube — never miss a new tutorial.

Planning an AI automation project or need a second opinion on your architecture?

Connect with me on LinkedIn — Lazar Milicevic, GenAI Engineer & bizflowai.io Founder.

Visit bizflowai.io for our services, case studies, and AI consulting.

Frequently asked questions

What is time-to-offer and why does it matter more than sourcing speed?

Time-to-offer is the gap between a candidate's last interview and a signed offer. It matters because candidates are lost after interviews, not during sourcing. At a 12-person staffing agency, this gap averaged 5.2 days, hammering close rates on senior roles because competing offers arrived first. Front-of-funnel automation like sourcing bots doesn't fix this bottleneck.

Why is averaging interview scorecard ratings a bad hiring practice?

Averaging 1-to-5 ratings across interviewers, which is what Greenhouse and Lever do, produces candidates who are fine at everything and great at nothing. It also rejects specialists you actually needed. Averaging smooths over disagreement and ignores competency weighting, so a strong technical hire can look identical to a mediocre generalist on paper.

How do I automate interview debrief synthesis using n8n?

Build four nodes: a Google Drive watcher that fires when the fourth scorecard lands or 48 hours pass, an OpenAI extraction node with a locked JSON schema pulling recommendations and verbatim quotes, a function node that applies role-specific competency weights from a Google Sheet, and a second model call that generates a one-page decision brief posted to Slack.

Why use a locked JSON schema instead of a freeform prompt for scorecard extraction?

A locked JSON schema forces the model to return exact fields: overall recommendation, per-competency scores, verbatim evidence quotes, and verbatim concerns. Freeform prompts let the model summarize and smooth over disagreement between interviewers. Preserving disagreement is the entire point, because split votes on specific competencies are the signal a hiring manager needs to make a real decision.

When should competency weights differ between roles?

Weights should differ for every distinct role because scoring formulas don't transfer. A senior backend engineer needs technical depth weighted at 3 and stakeholder communication at 0 or low. A customer success hire is the opposite. Store one row per role in a Google Sheet with six competency columns and weights from 0 to 3, then multiply each interviewer's scores by that role's weights.