n8n builds an agent out of nodes: a trigger starts the run, an AI Agent node reasons with the chat model plugged into it, and tool nodes let it read Jira and write Google Sheets. The chapter ships five importable workflows, from a QA chat assistant to a batch job that turns a CSV of Jira keys into test-case rows, plus ContentForge, a local app that does the content pipeline in code.
Importable JSON in n8n_AIAgent/, all exported inactive. The social agent also has a near-identical copy.
17
Nodes in the v2 JSON
The 10 nodes of v1 plus a form, a CSV extract, a loop, a Jira get, a second agent, a done node and a second sticky note.
11
Sheet columns
Upserted on Test Case ID, so a rerun updates rows instead of adding duplicates.
4
Model providers
Groq (Qwen), DeepSeek, OpenAI and Gemini nodes appear across the workflows.
01Why a tester cares
n8n is a workflow automation tool: you connect nodes on a canvas and data moves between them as a list of JSON items. Its AI Agent node turns a workflow into an agent. A trigger hands it a message, the chat model plugged into its model port decides what to do, and the tool nodes plugged into its tool port let it act: read a Jira ticket, write a row to Google Sheets, file a bug.
For a tester that means the glue work around test design (fetch the ticket, write the cases, put them where the team tracks them) becomes a workflow you can import, rerun and inspect node by node. The chapter also shows the limits: the model only does what the prompt and the wiring allow, and an exported workflow can say something different from its README.
Main connections carry items from node to node. The sub-nodes under the agent plug into typed ports; a model node that is not connected does nothing.
What the chapter ships: five importable workflows, ContentForge (a local Next.js app that runs a daily content pipeline into an Excel file), and two skill files that package repeatable instructions for an AI assistant.
02The five workflows
Every row below was read from the exported JSON (node types, parameters and connections), not from the README.
Workflow file
Trigger
Model, as wired
Tools and outputs
What it does
AI_3X_01_QA_Buddy.json
Chat, public, greets "Hi I am QABuddy!"
Groq qwen/qwen3-32b ("QWEN Brain")
None
Answers QA questions only; the system message rules out everything else.
AI_3X_02_JIRA_Agent.json
Chat, public
Groq qwen/qwen3-32b + Simple Memory
Jira tool: create a Bug; Summary and Description filled by the model
Turns a chat description into a Jira bug with steps to reproduce.
AI_3X_03_Read_PRD_TestCases_Excel.json
Chat (Schedule, Slack and Teams triggers present but disabled)
DeepSeek deepseek-v4-flash; a Groq "Brain" node is present but not connected
Jira tool: get issue. Google Sheets tool: append or update row
"JiraTestForge": on "create test cases for VWO-48" it fetches the ticket and writes 5 to 10 test cases, one row each.
AI_3X_04_Read_PRD_TestCases_Excel_v2.json
Form with a CSV upload (the v1 chat trigger is now disabled)
DeepSeek, shared by both agents
Jira get node in the main path; the same Sheets tool
Batch version: one ticket per CSV row through a loop, then a done node.
AI_3X_05_Social_media_AI agent.json
Schedule, daily at 9 AM
DeepSeek (topic), OpenAI gpt-5.5 (content), the Google Gemini node for images
Sheets append, read and update; Drive upload and share
A daily content run: one topic, five pieces of content, cover images, tracked in a sheet. The "(1)" copy differs only in the image model.
flowchart TB
F["Upload CSV with JIRA IDs (form)"] --> E["Extract JIRA IDs from CSV"] --> L{"Loop Over JIRA IDs"}
L -- loop --> J["Fetch Ticket Details (Jira get)"] --> A["Generate Test Cases with AI"]
A --> L
L -- done --> D["All Tickets Processed (Set)"]
M["DeepSeek Chat Model"] -. ai_languageModel .-> A
T["Append or update row in sheet"] -. ai_tool .-> A
AI_3X_04 v2, the CSV path. The loop sends one ticket at a time to the Jira node and the agent; when no items are left it takes the done branch.
Only connected sub-nodes count. In AI_3X_03 the Groq node called "Brain" sits on the canvas, but its connection list is empty: the agent runs on the DeepSeek Chat Model. Read the connections, not the node names.
03Live demo: run the batch workflow
This is the CSV path of AI_3X_04 with its real node names, parameters and expressions. Execute it, or step one node run at a time, and click any node to see the items it output, the way the n8n editor shows them. The Jira tickets are the chapter 13 fixtures; the test-case rows are written for this page, because a real model writes its own wording on every run.
Run the CSV batch workflow node by nodeNo API key needed
Canvas: the CSV path of AI_3X_04 v2 (click a node to see its output)
Try these. Execute twice: the second run updates the same 10 rows, because the tool matches on Test Case ID. Delete the jiraId header line and run: the first row becomes the header and the Jira node gets an empty key. Add VWO-99 as a third row: the batch stops at that ticket, and the rows already written stay in the sheet.
04Read the export: wiring, prompts and parameters
An exported workflow is plain JSON: a nodes array (type, version, parameters, and credentials by reference) and a connections object. Reading it is the fastest way to know what a workflow really does.
The agent's ports are connection types
In AI_3X_02 the trigger uses main; the model, the Jira tool and the memory each plug in through their own type.
$fromAI('Summary', ...) tells n8n that the model supplies this value when it calls the tool. The same mechanism fills all 11 sheet columns in the Sheets tool.
The v2 agent receives the ticket in its user message, with expressions resolved per item, and the JiraTestForge rules in its system message:
Generate Test Cases with AI: text
=Generate test cases for this Jira ticket:
Key: {{ $json.key }}
Summary: {{ $json.fields.summary }}
Description: {{ $json.fields.description }}
Create 5-10 test cases covering positive, negative, boundary, and edge cases. Write each test case to the Google Sheet using the tool.
Decoded from the agent node in AI_3X_04. The leading = marks an expression; {{ $json... }} reads the current item.
Generate Test Cases with AI: systemMessage
# ROLE
You are JiraTestForge, a QA test-case generation agent. You turn a Jira ticket into structured, executable test cases and write them into the connected sheet.
# WORKFLOW
1. READ the ticket data provided in the user message (key, summary, description, acceptance criteria).
2. GENERATE 5–10 test cases grounded ONLY in the ticket content:
- Positive / happy path
- Negative / invalid input
- Boundary & edge conditions
- Every acceptance criterion explicitly listed
3. WRITE TO SHEET
- For EACH test case, make ONE call to the "Append or update row in sheet" tool.
- One row per test case. Never pack multiple test cases into a single field.
# COLUMN SCHEMA
- Test Case ID : <KEY>-TC-01, <KEY>-TC-02, ... (sequential)
- Summary / Title : what is being tested, in one line
- Preconditions: state required before execution (or "None")
- Test Steps : numbered steps, one action per line
- Expected Result : precise expected outcome
- Actual Result : leave empty
- Status : Not Executed
- Priority : P1 | P2 | P3
- Assignee : leave empty
- Execution Date : leave empty
- Comments / Notes : leave empty
# RULES
- Every test case must trace back to the ticket. No hallucinated features.
- Steps must be atomic and concrete.
- After writing all rows, reply with: "Generated N test cases for <KEY>"
Decoded from the agent node in AI_3X_04, verbatim.
Prompt and sheet must agree
The tool can only write the columns it maps. The v1 chat prompt in AI_3X_03 lists a column schema that does not match the sheet; the v2 batch prompt lists the sheet's 11 columns exactly.
Sheet column (the tool writes these 11)
v1 chat prompt (AI_3X_03)
v2 batch prompt (AI_3X_04)
Test Case ID
yes
yes
Summary / Title
no: asks for Title
yes
Preconditions
yes
yes
Test Steps
yes
yes
Expected Result
yes
yes
Actual Result
no
yes, "leave empty"
Status
yes, Not Executed
yes, Not Executed
Priority
yes, P1 to P3
yes, P1 to P3
Assignee
no
yes, "leave empty"
Execution Date
no
yes, "leave empty"
Comments / Notes
no
yes, "leave empty"
not a sheet column
asks for Jira Key, Test Data, Type
none
The CSV path, parameter by parameter
AI_3X_04: Upload CSV with JIRA IDs
"parameters": {
"formTitle": "Upload JIRA IDs for Test Case Generation",
"formDescription": "Upload a CSV file containing JIRA ticket IDs. The workflow will generate test cases for each ticket.",
"formFields": {
"values": [
{
"fieldLabel": "CSV File with JIRA IDs",
"fieldType": "file",
"fieldName": "csvFile",
"multipleFiles": false,
"acceptFileTypes": ".csv",
"requiredField": true
}
]
},
"options": {
"appendAttribution": false,
"buttonLabel": "Generate Test Cases",
"respondWithOptions": {
"values": {
"formSubmittedText": "Processing your JIRA IDs. Test cases will be generated shortly."
}
}
}
},
Extract JIRA IDs from CSV reads the binary field csvFile with "headerRow": true, so each data row becomes one item keyed by the header. Fetch Ticket Details then gets "issueKey": "={{ $json.jiraId }}". The loop's two outputs are wired like this, output 0 first:
AI_3X_05 runs every day at 9 AM with no one at the keyboard. It writes a topic into a sheet, generates five pieces of content and three cover images, uploads the images to Google Drive and updates the same sheet row.
AI_3X_05 as wired: each model node has one fixed job.
Reading the JSON against the notes shows four differences worth knowing before you activate it:
Each model has its own job. DeepSeek writes the topic, OpenAI gpt-5.5 writes the content through a structured output parser with five keys (linkedinPost, mediumArticle, igScript, ytScript, devtoArticle), and the Gemini node only makes images. A second DeepSeek node is not connected. The top-level README calls them swappable backends of one agent; the JSON does not work that way.
Nothing is posted and no email is sent. The sticky note lists "Schedule and POST" and an email when the draft is ready, but the workflow ends at the sheet update. Drafts wait in the sheet for a human, which is the safer design anyway.
Check the update mapping.Agent 4 - Sheet Updater matches rows on Date, but Date is not among the values it maps.
The images become public.Share Image File grants reader access to anyone, with file discovery on.
Measure, do not trust, length instructions. The JSON keeps four sample outputs of the content writer (pinData). The prompt asks for a 3000-word Medium article; the four came back at 2,121 to 2,407 words. Dev.to asked for 2000 and got 1,443 to 1,813; two of the four YouTube scripts went over the 1000-word cap.
06ContentForge: the same pipeline in code
social_ai_agent/contentforge/ is a local Next.js 14 + TypeScript dashboard that does the content run without n8n: Topic Generator, then Content Writer (LinkedIn, Medium, Instagram, YouTube and Dev.to with Groq), then Image Generator (Gemini). Every step writes straight back to content_calendar.xlsx, and the row's status moves Pending, Writing, Imaging, Done (or Error).
flowchart TB
C["cron 09:00 or POST /api/run"] --> P["runPipeline, one at a time"] --> T["Topic Generator"] --> W["Content Writer, 5 pieces"] --> I["Image Generator"]
T -. Pending .-> X[("content_calendar.xlsx")]
W -. Writing .-> X
I -. Imaging, then Done .-> X
One row per date. The scheduler runs at 09:00 local time; the dashboard button calls the same runPipeline().
terminal
cd chapter_04_AI_Agents_n8n/social_ai_agent/contentforge
npm install
cp .env.example .env.local # then add GROQ_API_KEY and GEMINI_API_KEYnpm run dev # http://localhost:3000npm run scheduler # optional: the 9 AM scheduler as its own Node process
Node.js 20 or newer. Keys by name: GROQ_API_KEY and GEMINI_API_KEY, with optional GROQ_MODEL (default llama-3.3-70b-versatile) and GEMINI_IMAGE_MODEL; .env.local wins over .env. The API: POST /api/run, GET /api/calendar, /api/today, /api/status, /api/log and /api/download (the workbook).
Two guards that keep the workbook sane
A second click on "Run Pipeline Now" while a run is in progress gets the same promise back instead of starting another run:
When the topic call fails (a missing key, or a duplicate topic) the agent still records a row, using a deterministic fallback title:
lib/agents.ts
functionfallbackTopic(existingTopics: string[], date: string): string {
const existing = newSet(existingTopics.map((topic) => topic.toLowerCase()));
for (const keyword of KEYWORD_POOL) {
const candidate = `${keyword} field notes for ${date}`;
if (!existing.has(candidate.toLowerCase())) {
return candidate;
}
}
return`AI testing field notes for ${date}`;
}
07Skill files: instructions you can reuse
A skill is a Markdown file with a short header (name and description) that tells an AI assistant when to load it, and a body that holds the rules. The chapter has two.
skillfile_content_generation/SKILL.md (testing-academy-content-engine): give it one topic and it produces a pack of seven pieces: a LinkedIn post, a Medium article, a YouTube script, an Instagram carousel script and three image prompts. The body is organised as voice rules, one section per deliverable, image styles, content threads, operating principles and an output checklist. brand-voice.md adds voice principles, an 8-beat video structure (hook, promise, why now, plain definition, how-to, payoff, reframe, call to action) and a scripting checklist.
A dated output pack.output/2026-06-14/ holds one real run, "Your AI Agent Needs a QA Contract, Not More Prompts": the topic, the LinkedIn post, the Medium article, the YouTube script, the carousel copy, the image prompts and a README.
resume-tailor/: a four-phase skill that scores a resume, cross-references ATS keywords against a job description, asks the candidate to confirm any skill that is not already evidenced, then rebuilds a single-column .docx with scripts/build_resume.js. Its one hard rule: never invent experience. The skill is marked proprietary, so this page describes it rather than reproducing it; its validation steps also assume a hosted sandbox, not a local machine.
The QA angle. A skill is a contract like a system prompt: an output checklist you can verify mechanically (banned phrases, word counts, required sections) turns "sounds good" into pass or fail.
08Import, credentials and what to watch
To use a workflow: open n8n Cloud or your own n8n, go to Workflows, import the JSON from n8n_AIAgent/, open every credential-backed node and connect your own account, then save and run the trigger. Credentials you may need, by type: Groq or DeepSeek (and OpenAI and Google Gemini for the social agent), Jira Software Cloud, Google Sheets OAuth2, Google Drive OAuth2, and Slack or Microsoft Teams only if you enable those triggers.
Re-select your own targets. The Jira project, issue type and Google Sheet in the export point at the author's accounts. Pick yours in each node before the first run.
Public chat triggers.AI_3X_01 and AI_3X_02 have "public": true. Once active, anyone with the URL can spend your model quota, and in 02 create Jira issues. Turn public off or add authentication.
The CSV needs a jiraId header. The Jira node reads {{ $json.jiraId }}. Without the header the first key becomes a column name.
One bad key stops the batch. No node sets "continue on fail", so a 404 from Jira ends the execution. Rows written for earlier tickets stay.
Set runs once per item. The done branch carries one item per ticket, so All Tickets Processed outputs its summary once per ticket. Turn on "Execute Once" in the node settings if you want a single summary.
Keep secrets in credentials. Never paste a key into a node parameter: an exported workflow carries every parameter with it.
DDrills for the chapter
Code drills use chapter_04_AI_Agents_n8n: open the JSON files in an editor or import them into n8n. Playwright drills target the live demo on the Page tab; turn on Show locator badges to see every data-testid.
Which model really runs?
In AI_3X_03, two chat-model nodes sit next to the agent. Which one does the agent use, and how can you tell from the JSON?
Expected result
The DeepSeek Chat Model (deepseek-v4-flash). Only it appears under connections with type ai_languageModel; the Groq "Brain" (qwen/qwen3-32b) has no connection.
Diff the prompt against the sheet
Compare the COLUMN SCHEMA in the v1 JiraTestForge prompt with the 11 columns the Sheets tool maps.
Expected result
Prompt-only fields: Jira Key, Title, Test Data, Type. Sheet-only columns: Summary / Title, Actual Result, Assignee, Execution Date, Comments / Notes. The v2 batch prompt lists the 11 sheet columns exactly.
Shape the CSV
What must the uploaded file look like for the v2 batch path to fetch every ticket?
Expected result
Comma-separated, with a header row containing a jiraId column and one Jira key per row. Extract From File uses the header row as item keys, and the Jira node reads {{ $json.jiraId }}.
Measure the pinned content
Count the words in the four pinned outputs of Agent 2 - Content Writer in AI_3X_05 and compare them with the prompt.
Hint
The outputs are under pinData. Split each field on whitespace.
Expected result
Medium: 2,121 to 2,407 words against 3000 asked. Dev.to: 1,443 to 1,813 against 2000. YouTube: 956 to 1,069 against 800 to 1000, so two are over. LinkedIn (351 to 375 against 300 to 500) is the only field always in range.
A failed first run
ContentForge runs once with GROQ_API_KEY missing. You add the key and click "Run Pipeline Now" on the same day. Which topic does it write about?
Expected result
The fallback written by the failed run, QA field notes for <today> if the sheet had no topics before. TopicGeneratorAgent reuses today's row whenever it already has a topic, then the row moves Writing, Imaging, Done.
Who fills the tool?
In the Jira tool of AI_3X_02, who decides the Summary and Description of the new bug?
Expected result
The model. Both fields are $fromAI(...) expressions, so the agent supplies them when it calls the tool. The project and issue type are fixed in the node.
Prove the upsertPlaywright
Execute the demo workflow twice and assert what the sheet reports each time.
Hint
Locators: n8n-run, n8n-sheet-status, and the rows of n8n-sheet.
Expected result
First run: appended 10, updated 0. Second run: appended 0, updated 10, and the table still has 11 rows (header plus 10).
Break the headerPlaywright
Fill n8n-csv with two keys and no header, execute, and assert where the run stopped and what Extract produced.
Expected result
n8n-node-fetch has the class is-error, n8n-status mentions no jiraId field, the sheet has only its header row, and the Extract node's output is [{ "VWO-48": "VWO-49" }].
Count the tool callsPlaywright
Execute the workflow and count the execution-log entries for sheet writes on VWO-48.
Hint
Log rows are li elements with data-kind; sheet writes start with appendOrUpdate.
Expected result
Five: appendOrUpdate VWO-48-TC-01 to VWO-48-TC-05, one tool call per test case, as the prompt demands.
SSolutions: the demo spec and the agent prompts
The Playwright spec passes against this page as written. The other tabs hold the prompts and wiring decoded from the workflow JSON, and ContentForge's pipeline.
tests/n8n-agents-demo.spec.ts
import { test, expect } from'@playwright/test';
const URL = 'https://app.thetestingacademy.com/ai/blueprint/learn/n8n-agents.html';
test('the batch writes five rows per ticket and ends on the done branch', async ({ page }) => {
await page.goto(URL);
await page.getByTestId('n8n-run').click();
awaitexpect(page.getByTestId('n8n-status')).toContainText('Workflow executed successfully');
awaitexpect(page.getByTestId('n8n-sheet').getByRole('row')).toHaveCount(11); // header + 10 rowsawait page.getByTestId('n8n-node-done').click();
awaitexpect(page.getByTestId('n8n-items')).toContainText('All JIRA tickets processed. Test cases generated.');
});
test('a second execution updates the rows instead of appending', async ({ page }) => {
await page.goto(URL);
await page.getByTestId('n8n-run').click();
awaitexpect(page.getByTestId('n8n-sheet-status')).toContainText('appended 10, updated 0');
await page.getByTestId('n8n-run').click();
awaitexpect(page.getByTestId('n8n-sheet-status')).toContainText('appended 0, updated 10');
awaitexpect(page.getByTestId('n8n-sheet').getByRole('row')).toHaveCount(11);
});
test('a CSV without the jiraId header stops at Fetch Ticket Details', async ({ page }) => {
await page.goto(URL);
await page.getByTestId('n8n-csv').fill('VWO-48\nVWO-49');
await page.getByTestId('n8n-run').click();
awaitexpect(page.getByTestId('n8n-node-fetch')).toHaveClass(/is-error/);
awaitexpect(page.getByTestId('n8n-status')).toContainText('no jiraId field');
awaitexpect(page.getByTestId('n8n-sheet').getByRole('row')).toHaveCount(1);
await page.getByTestId('n8n-node-extract').click();
awaitexpect(page.getByTestId('n8n-items')).toContainText('"VWO-48": "VWO-49"');
});
AI_3X_04: Generate Test Cases with AI (systemMessage)
# ROLE
You are JiraTestForge, a QA test-case generation agent. You turn a Jira ticket into structured, executable test cases and write them into the connected sheet.
# WORKFLOW
1. READ the ticket data provided in the user message (key, summary, description, acceptance criteria).
2. GENERATE 5–10 test cases grounded ONLY in the ticket content:
- Positive / happy path
- Negative / invalid input
- Boundary & edge conditions
- Every acceptance criterion explicitly listed
3. WRITE TO SHEET
- For EACH test case, make ONE call to the "Append or update row in sheet" tool.
- One row per test case. Never pack multiple test cases into a single field.
# COLUMN SCHEMA
- Test Case ID : <KEY>-TC-01, <KEY>-TC-02, ... (sequential)
- Summary / Title : what is being tested, in one line
- Preconditions: state required before execution (or "None")
- Test Steps : numbered steps, one action per line
- Expected Result : precise expected outcome
- Actual Result : leave empty
- Status : Not Executed
- Priority : P1 | P2 | P3
- Assignee : leave empty
- Execution Date : leave empty
- Comments / Notes : leave empty
# RULES
- Every test case must trace back to the ticket. No hallucinated features.
- Steps must be atomic and concrete.
- After writing all rows, reply with: "Generated N test cases for <KEY>"
AI_3X_04: Generate Test Cases with AI (text)
=Generate test cases for this Jira ticket:
Key: {{ $json.key }}
Summary: {{ $json.fields.summary }}
Description: {{ $json.fields.description }}
Create 5-10 test cases covering positive, negative, boundary, and edge cases. Write each test case to the Google Sheet using the tool.
AI_3X_03: AI Agent (systemMessage)
# ROLE
You are JiraTestForge, a QA test-case generation agent. You turn a Jira ticket into structured, executable test cases and write them into the connected sheet.
# WHEN TO ACT
Trigger the full workflow only when the user's message expresses intent to create test cases (e.g. "create test case", "generate test cases", "make TCs"). For any other message, reply conversationally and do NOT call any tool.
# WORKFLOW - follow in this exact order
1. EXTRACT KEY
- Find the Jira issue key in the user's message. It matches the pattern [A-Z]+-[0-9]+ (e.g. VWO-1234, PROJ-87).
- If no key is present, ask the user for the ticket key and stop. Never invent a key.
- If multiple keys are present, process each one in turn.
2. FETCH TICKET
- Call the "Fetch PRD by Ticket ID" tool with the extracted key.
- Read the summary, description, and acceptance criteria from the response.
3. GENERATE TEST CASES (ground them ONLY in the fetched ticket)
- Do not assume requirements, fields, or flows that are not in the ticket.
- Cover: positive / happy path, negative / invalid input, boundary & edge conditions, and every acceptance criterion explicitly listed.
- Produce 5–10 test cases depending on ticket size. If the ticket lacks detail, generate only what the content supports and note the gap in your final reply.
4. WRITE TO SHEET
- For EACH test case, make ONE call to the "Append or update row in sheet" tool.
- One row per test case. Never pack multiple test cases into a single field.
# COLUMN SCHEMA (must match the sheet's header row)
- Test Case ID : <KEY>-TC-01, <KEY>-TC-02, ... (sequential)
- Jira Key : the ticket key
- Title : what is being tested, in one line
- Preconditions: state required before execution (or "None")
- Test Steps : numbered steps, one action per line
- Test Data : inputs used (or "N/A")
- Expected Result : precise expected outcome
- Type : Positive | Negative | Edge | Boundary
- Priority : P1 | P2 | P3
- Status : Not Executed
# RULES
- Every test case must trace back to the ticket. No hallucinated features.
- Steps must be atomic and concrete - a tester follows them without guessing.
- After writing all rows, reply with a short summary: ticket key, count of test cases created, and a one-line list of their titles.
Decoded from the workflow JSON. Verbatim, except that long dashes are printed as hyphens.