Chapter 4
Chapter 4 . Agents and workflows . n8n agents

n8n agents: a trigger, a model, tools

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.

5
n8n workflows
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.

Anatomy of an n8n AI Agent node A trigger feeds the AI Agent through its main input and the agent passes an output item to the next node. Four typed ports under the agent take a chat model, memory, tools and an output parser. Trigger chat, form or schedule main AI Agent system message = the contract main Next node { output: "..." } Chat model ai_languageModel Groq, DeepSeek, OpenAI Memory ai_memory Simple Memory Tools ai_tool Jira, Google Sheets Output parser ai_outputParser fixed JSON keys
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 fileTriggerModel, as wiredTools and outputsWhat it does
AI_3X_01_QA_Buddy.jsonChat, public, greets "Hi I am QABuddy!"Groq qwen/qwen3-32b ("QWEN Brain")NoneAnswers QA questions only; the system message rules out everything else.
AI_3X_02_JIRA_Agent.jsonChat, publicGroq qwen/qwen3-32b + Simple MemoryJira tool: create a Bug; Summary and Description filled by the modelTurns a chat description into a Jira bug with steps to reproduce.
AI_3X_03_Read_PRD_TestCases_Excel.jsonChat (Schedule, Slack and Teams triggers present but disabled)DeepSeek deepseek-v4-flash; a Groq "Brain" node is present but not connectedJira 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.jsonForm with a CSV upload (the v1 chat trigger is now disabled)DeepSeek, shared by both agentsJira get node in the main path; the same Sheets toolBatch version: one ticket per CSV row through a loop, then a done node.
AI_3X_05_Social_media_AI agent.jsonSchedule, daily at 9 AMDeepSeek (topic), OpenAI gpt-5.5 (content), the Google Gemini node for imagesSheets append, read and update; Drive upload and shareA 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)
Loop Over JIRA IDs, done output
Plugged into the agent
data-testid=n8n-csvdata-testid=n8n-rundata-testid=n8n-stepdata-testid=n8n-resetdata-testid=n8n-statusdata-testid=n8n-itemsdata-testid=n8n-sheetdata-testid=n8n-sheet-statusdata-testid=n8n-node-fetchdata-testid=n8n-node-done
Execution log
    Output items
    
          Agent prompt for the current item
          
    
        
    Google Sheet, Sheet1 (written only by the tool)
    Test Case IDSummary / TitlePreconditionsTest StepsExpected ResultActual ResultStatusPriorityAssigneeExecution DateComments / Notes
    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.

    AI_3X_02_JIRA_Agent.json
      "connections": {
        "When chat message received": {
          "main": [
            [
              {
                "node": "AI Agent",
                "type": "main",
                "index": 0
              }
            ]
          ]
        },
        "QWEN Brain": {
          "ai_languageModel": [
            [
              {
                "node": "AI Agent",
                "type": "ai_languageModel",
                "index": 0
              }
            ]
          ]
        },
        "Create JIRA ticket in the VWO project": {
          "ai_tool": [
            [
              {
                "node": "AI Agent",
                "type": "ai_tool",
                "index": 0
              }
            ]
          ]
        },
        "Simple Memory": {
          "ai_memory": [
            [
              {
                "node": "AI Agent",
                "type": "ai_memory",
                "index": 0
              }
            ]
          ]
        }
      },

    Let the model fill tool parameters

    $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.

    AI_3X_02_JIRA_Agent.json
            "summary": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Summary', ``, 'string') }}",
            "additionalFields": {
              "description": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Description', ``, 'string') }}"
            }
          },
          "type": "n8n-nodes-base.jiraTool",
          "typeVersion": 1,

    The batch agent's prompt

    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 IDyesyes
    Summary / Titleno: asks for Titleyes
    Preconditionsyesyes
    Test Stepsyesyes
    Expected Resultyesyes
    Actual Resultnoyes, "leave empty"
    Statusyes, Not Executedyes, Not Executed
    Priorityyes, P1 to P3yes, P1 to P3
    Assigneenoyes, "leave empty"
    Execution Datenoyes, "leave empty"
    Comments / Notesnoyes, "leave empty"
    not a sheet columnasks for Jira Key, Test Data, Typenone

    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_04: Loop Over JIRA IDs connections
        "Loop Over JIRA IDs": {
          "main": [
            [
              {
                "node": "All Tickets Processed",
                "type": "main",
                "index": 0
              }
            ],
            [
              {
                "node": "Fetch Ticket Details",
                "type": "main",
                "index": 0
              }
            ]
          ]
        }

    05The scheduled content agent

    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.

    flowchart TB
      S["Daily at 9 AM"] --> A1["Agent 1: topic"] --> W["Sheet: append, Pending"] --> A2["Agent 2: content"] --> A3["Agent 3: images, Gemini"] --> DR["Drive: upload and share"] --> A4["Sheet: update, Generated"]
      DS["DeepSeek Chat Model1"] -. model .-> A1
      OA["OpenAI gpt-5.5 + parser"] -. model .-> A2
    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_KEY
    npm run dev                    # http://localhost:3000
    npm 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:

    lib/pipeline.ts
    export function runPipeline(date = formatLocalDate()): Promise<PipelineRunResult> {
      if (activeRun) {
        return activeRun;
      }
    
      activeRun = executePipeline(date).finally(() => {
        activeRun = null;
      });
    
      return activeRun;
    }

    Every read and write of the workbook waits for the previous one, so two writers never interleave:

    lib/excelManager.ts
      private async withLock<T>(operation: () => Promise<T>): Promise<T> {
        const run = this.queue.then(operation, operation);
        this.queue = run.then(
          () => undefined,
          () => undefined
        );
        return 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
    function fallbackTopic(existingTopics: string[], date: string): string {
      const existing = new Set(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.