n8n tutorial - Lesson 24: Quality Gate Pattern in n8n: AI Review Before Publishing

Hi everyone, in this post we're building a Quality Gate inside an n8n Content Factory workflow — an AI-powered review layer that blocks low-quality content before it ever gets published. This is a core pattern in n8n quality control automation and one of the most practical additions you can make to any content pipeline.

How to do:

Step 1 — Design the AI Review Checklist (Blog + YouTube)

Before adding any nodes, define exactly what the AI will check and what counts as a passing score.
  1. For the Blog review, define 5 criteria: word_count, has_headings, has_keyword, no_placeholder, title_length.
  2. For the YouTube review, define 5 criteria: title_length, description_length, has_tags, has_timestamps, no_empty_field.
  3. Set the pass threshold at ≥ 4 out of 5 criteria met.
  4. Require the AI to return a structured JSON object with three fields: pass, score, and notes.

Tip — Locking down the output schema before building the nodes saves debugging time later. The pass field will drive your IF node, score goes to the rejected log, and notes tells you exactly why a piece failed.

Step 2 — Insert AI Review Nodes into Each Child Workflow

Add an AI review node to both the Blog and YouTube child workflows, placing each one at the right point in the chain.
  1. In T6-Content-Child-Blog, insert an AI Review Blog node between the Format HTML node and the POST Draft Blogger node.
  2. In T6-Content-Child-YouTube, insert an AI Review YouTube node at the equivalent position — after content is fully formatted.
  3. In the User message of AI Review YouTube, wrap array fields with JSON.stringify():
    • Tags: JSON.stringify($('YouTube SEO').item.json.output.tags)
    • Timestamps: JSON.stringify($('YouTube SEO').item.json.output.timestamps)

Note — If you pass an array directly into a User message expression without JSON.stringify(), n8n renders it as [object Object] and the AI cannot read the data. Always stringify arrays before injecting them into prompt strings.

Step 3 — Add Structured Output Parsers

Attach a Structured Output Parser to each AI review node so the response always comes back as clean, typed JSON.
  1. For AI Review Blog: use the Schema (JSON string) method in the parser.
  2. For AI Review YouTube: use the Generate From JSON Example method in the parser.
  3. Provide an example JSON like {"pass": true, "score": 4, "notes": "missing keyword"} so n8n infers the correct types.

Note — These two parser methods produce different output types for the pass field. The Schema method returns "true" as a string; the Generate From JSON Example method returns true as a boolean. You must configure the IF node to match the correct type for each workflow.

Step 4 — Add IF Nodes to Route Pass vs. Fail

Insert an IF node after each review node to split the workflow into a passing branch and a failing branch.
  1. In T6-Content-Child-Blog, add an IF node named Check Pass.
  2. Set its condition on $json.pass:
    • Because the Blog parser returns a string, set the condition to String → equals → "true" — not Boolean.
  3. In T6-Content-Child-YouTube, add an IF node named Check Pass YT.
  4. Set its condition to Boolean → is true because the YouTube parser returns a real boolean.
  5. Connect the True branch of each IF node to the existing publish nodes (POST Draft Blogger, Create Google Doc).
  6. Connect the False branch of each IF node to a Google Sheets Append node targeting the Sheet T6-Rejected.

Tip — Mixing up string "true" and boolean true in IF conditions is one of the most common silent bugs in n8n. If your IF node always routes to the False branch despite the AI passing content, this type mismatch is the first thing to check.

Step 5 — Fix Cross-Node References After IF Node Insertion

After inserting the IF node, all downstream nodes lose their direct $json context — this is a critical gotcha in this n8n tutorial.
  1. Understand what changed: after the IF node, $json inside downstream nodes refers to the IF node's output (only the review result), not the original formatted content.
  2. In POST Draft Blogger, replace any $json.xxx references with explicit cross-node refs, for example:
    • $('Format HTML').item.json.title
    • $('Format HTML').item.json.html_content
  3. Apply the same fix to Create Google Doc — reference the correct upstream node by name for every field it needs.
  4. For the YouTube child workflow, confirm that Create Google Doc is placed after the IF node, not before it. An earlier misplacement caused it to run regardless of review outcome.

Note — Cross-node references like $('NodeName').item.json.field are the reliable way to reach data from any earlier node in the chain. Make this your default approach whenever the data path passes through a branching node like IF, Switch, or Merge.

Step 6 — Fix HTML Content in POST Draft Blogger Body

Passing HTML as a raw JSON string in the request body breaks when the content contains special characters.
  1. Identify the problem: html_content contains double quotes and newlines, which corrupt the raw JSON string body.
  2. Switch the POST Draft Blogger node's body mode from Raw JSON string to Body Parameters (key-value pairs).
  3. Map each field (title, content, labels, etc.) as a separate key-value entry — n8n handles escaping automatically in this mode.

Tip — Whenever you're sending HTML or any user-generated text in an HTTP request body, key-value / Body Parameters mode is safer than raw JSON strings. n8n escapes the values for you, eliminating an entire class of encoding bugs.

Step 7 — Set Up the T6-Rejected Sheet Log

Route failed content to a dedicated Google Sheet so you can review and fix it later.
  1. Create a new Google Sheet named T6-Rejected.
  2. Define these columns: timestamp, topic, score, notes.
  3. In the False branch of each IF node, connect a Google Sheets → Append Row node targeting this sheet.
  4. Map the fields:
    • timestamp: {{ $now }}
    • topic: cross-node ref to the topic field from the trigger
    • score: $json.score
    • notes: $json.notes

Step 8 — Pass row_number into Child Workflows and Fix Mark Done

Matching sheet rows by topic string is fragile; switching to row_number makes the Mark Done update reliable.
  1. Open the child workflow's trigger node (When Executed by Another Workflow) and add a new input field: row_number (type: Number).
  2. In the parent workflow (T6-Content-Factory-Dispatch), open the Call Child Blog Workflow node and click Refresh Input List to see the new field.
  3. Map the parent's row_number value into the new field.
  4. In the Mark Done node, change the row-matching logic from topic string to row_number (number match).

Tip — String-based row matching fails silently when there's a whitespace difference or encoding mismatch between the sheet and the workflow variable — you get "No output data returned" with no clear error. Number-based matching with row_number is deterministic and always safe.

Step 9 — Test End-to-End: Pass and Fail Paths

Run a full test covering both branches to confirm the quality gate works correctly.
  1. Trigger the dispatch workflow with a topic that will produce high-quality content (score ≥ 4).
  2. Verify the True branch executes: Blogger draft is posted, Google Doc is created, Sheet status is updated to done.
  3. Temporarily lower the pass threshold or submit a topic with missing fields to force a fail.
  4. Verify the False branch executes: a new row appears in T6-Rejected with correct timestamp, topic, score, and notes.
  5. Confirm Mark Done updates the correct row (matched by row_number, not topic string).

Key Lessons from This Session

  1. Cross-node refs break after IF nodes. Once your data path passes through a branching node, $json no longer points to earlier content — always use $('NodeName').item.json.field explicitly.
  2. HTML in raw JSON bodies causes silent corruption. Switch to Body Parameters (key-value) mode and let n8n handle escaping automatically.
  3. Structured Output Parser type depends on the method used. Schema JSON string → returns pass as a string; Generate From JSON Example → returns pass as a boolean. Your IF condition must match the actual type.
  4. Child workflow inputs require declaration in the trigger first. The parent cannot pass a new field like row_number until the child's trigger node declares it, followed by a Refresh Input List in the parent.
  5. Use row_number, not topic strings, for sheet row matching. String matching fails silently on whitespace or encoding differences; number matching is deterministic.
  6. Always JSON.stringify arrays in prompt expressions. Arrays passed raw into User message expressions render as [object Object], making them unreadable to the AI model.

Conclusion:

In this n8n workflow automation tutorial, we added a full AI-powered quality gate to a Content Factory — covering checklist design, structured output parsing, IF-based routing, rejected content logging, and reliable row matching. These patterns make your automation genuinely production-ready by catching bad content before it ever reaches a live channel. Next session we move into Week 7 and start building our first AI Agent in n8n.

If you have any questions, feel free to leave a comment below. Thank you!

Tags: n8n quality control automation, n8n tutorial, n8n workflow automation, AI review workflow, content factory n8n, structured output parser n8n, quality gate pattern, n8n IF node

Maybe you are interested!