Skip to content
AI Job Scout

Module 11 · ai · intermediate

learner.json

In n8n, copy this file and paste it onto the canvas, or use Import from File.

{
  "name": "11 - AI Job Scout",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 4
            }
          ]
        }
      },
      "id": "m11-schedule",
      "name": "Every 4 Hours",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "path": "jobs/scout",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "m11-webhook",
      "name": "Scout API",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        200
      ],
      "webhookId": "course-11-scout"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s1",
              "name": "keywords",
              "value": "={{ ($json.query?.keywords ?? \"n8n,zapier,make.com,automation engineer,workflow automation,ai automation,ai agent,llm,integration engineer,integrations engineer,no-code,low-code\").split(\",\").map(k => k.trim().toLowerCase()).filter(Boolean) }}",
              "type": "array"
            },
            {
              "id": "s2",
              "name": "min_score",
              "value": "={{ Number($json.query?.min_score ?? 60) }}",
              "type": "number"
            },
            {
              "id": "s3",
              "name": "max_jobs",
              "value": "={{ Math.min(Number($json.query?.limit ?? 10), 25) }}",
              "type": "number"
            },
            {
              "id": "s4",
              "name": "dry_run",
              "value": "={{ $json.query?.dry_run === 'true' }}",
              "type": "boolean"
            },
            {
              "id": "s5",
              "name": "profile",
              "value": "Full-stack developer (TypeScript, Node.js, React, Python) specializing in n8n automations, AI agents and RAG (OpenAI), API integrations, Google Workspace and Supabase. Based in the Philippines (UTC+8), works fully remote, fluent English. Prefers freelance/contract work or remote roles open to Asia-based candidates.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "m11-settings",
      "name": "Settings (edit profile)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        100
      ]
    },
    {
      "parameters": {
        "url": "https://remoteok.com/api",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (n8n job scout)"
            }
          ]
        },
        "options": {
          "timeout": 20000
        }
      },
      "id": "m11-rok",
      "name": "Fetch RemoteOK",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        0
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "url": "https://weworkremotely.com/categories/remote-programming-jobs.rss",
        "options": {}
      },
      "id": "m11-wwr",
      "name": "Fetch We Work Remotely",
      "type": "n8n-nodes-base.rssFeedRead",
      "typeVersion": 1.2,
      "position": [
        440,
        200
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// RemoteOK returns an array; n8n splits it into items. The first item is a legal notice.\nconst strip = (html) => String(html ?? '').replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/gi, ' ').replace(/\\s+/g, ' ').trim();\nreturn $input.all()\n  .filter(item => item.json.position)\n  .map(({ json: j }) => ({\n    json: {\n      source: 'remoteok',\n      title: j.position,\n      company: j.company ?? '',\n      url: j.url,\n      tags: (j.tags ?? []).map(t => String(t).toLowerCase()),\n      location: j.location ?? '',\n      salary: j.salary_min ? `$${j.salary_min}-${j.salary_max}` : '',\n      posted_at: j.date,\n      description: strip(j.description).slice(0, 1500),\n    },\n  }));"
      },
      "id": "m11-norm-rok",
      "name": "Normalize RemoteOK",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// WWR titles look like \"Company: Job Title\".\nreturn $input.all().filter(item => item.json.title).map(({ json: j }) => {\n  const [company, ...rest] = String(j.title ?? '').split(': ');\n  return {\n    json: {\n      source: 'weworkremotely',\n      title: rest.length ? rest.join(': ') : company,\n      company: rest.length ? company : '',\n      url: j.link,\n      tags: (j.categories ?? []).map(t => String(t).toLowerCase()),\n      location: '',\n      salary: '',\n      posted_at: j.isoDate ?? j.pubDate,\n      description: String(j.contentSnippet ?? j.content ?? '').replace(/\\s+/g, ' ').trim().slice(0, 1500),\n    },\n  };\n});"
      },
      "id": "m11-norm-wwr",
      "name": "Normalize WWR",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        200
      ]
    },
    {
      "parameters": {},
      "id": "m11-merge",
      "name": "All Jobs",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        880,
        100
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "k1",
              "leftValue": "={{ $('Settings (edit profile)').first().json.keywords.some(k => new RegExp('(^|[^a-z0-9])' + k.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + 's?($|[^a-z0-9])').test([$json.title, $json.tags.join(' '), $json.description].join(' ').toLowerCase())) }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "m11-keywords",
      "name": "Matches Keywords",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.2,
      "position": [
        1100,
        100
      ]
    },
    {
      "parameters": {
        "sortFieldsUi": {
          "sortField": [
            {
              "fieldName": "posted_at",
              "order": "descending"
            }
          ]
        },
        "options": {}
      },
      "id": "m11-newest",
      "name": "Newest First",
      "type": "n8n-nodes-base.sort",
      "typeVersion": 1,
      "position": [
        1320,
        100
      ]
    },
    {
      "parameters": {
        "maxItems": "={{ $('Settings (edit profile)').first().json.max_jobs }}"
      },
      "id": "m11-limit",
      "name": "Top N",
      "type": "n8n-nodes-base.limit",
      "typeVersion": 1,
      "position": [
        1540,
        100
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "d1",
              "leftValue": "={{ $('Settings (edit profile)').first().json.dry_run }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "m11-dry",
      "name": "Dry Run?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1760,
        100
      ]
    },
    {
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "jobs",
        "options": {}
      },
      "id": "m11-agg-dry",
      "name": "Collect Matches",
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        1980,
        -60
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { dry_run: true, count: $json.jobs.length, jobs: $json.jobs.map(j => ({ source: j.source, title: j.title, company: j.company, url: j.url, posted_at: j.posted_at })) } }}",
        "options": {}
      },
      "id": "m11-respond-dry",
      "name": "Respond Matches",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2200,
        -60
      ]
    },
    {
      "parameters": {
        "text": "=CANDIDATE PROFILE:\n{{ $('Settings (edit profile)').first().json.profile }}\n\nJOB:\nTitle: {{ $json.title }}\nCompany: {{ $json.company }}\nTags: {{ $json.tags.join(', ') }}\nLocation: {{ $json.location || 'not stated' }}\nSalary: {{ $json.salary || 'not stated' }}\nDescription: {{ $json.description }}",
        "attributes": {
          "attributes": [
            {
              "name": "score",
              "type": "number",
              "description": "0-100: how well this job fits the candidate. 80+ = strong fit worth applying today, 60-79 = possible, below 60 = poor fit. Penalize hard if location/timezone rules out the candidate.",
              "required": true
            },
            {
              "name": "fit_reasons",
              "description": "One sentence: the strongest reasons it fits (or doesn't)",
              "required": true
            },
            {
              "name": "red_flags",
              "description": "Short comma-separated list of concerns (location limits, seniority mismatch, unpaid trial, vague scope), or 'none'",
              "required": true
            },
            {
              "name": "pitch_angle",
              "description": "One sentence the candidate could open their application with, referencing a specific need from the job",
              "required": true
            }
          ]
        },
        "options": {
          "systemPromptTemplate": "You are a blunt, practical career advisor for freelance automation developers. Score jobs strictly against the candidate profile. Never invent facts about the job."
        }
      },
      "id": "m11-score",
      "name": "Score Job",
      "type": "@n8n/n8n-nodes-langchain.informationExtractor",
      "typeVersion": 1.2,
      "position": [
        1980,
        160
      ]
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-mini",
          "cachedResultName": "gpt-5-mini"
        },
        "options": {}
      },
      "id": "m11-model",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.3,
      "position": [
        1980,
        380
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "j1",
              "name": "title",
              "value": "={{ $('Top N').item.json.title }}",
              "type": "string"
            },
            {
              "id": "j2",
              "name": "company",
              "value": "={{ $('Top N').item.json.company }}",
              "type": "string"
            },
            {
              "id": "j3",
              "name": "url",
              "value": "={{ $('Top N').item.json.url }}",
              "type": "string"
            },
            {
              "id": "j4",
              "name": "source",
              "value": "={{ $('Top N').item.json.source }}",
              "type": "string"
            },
            {
              "id": "j5",
              "name": "posted_at",
              "value": "={{ $('Top N').item.json.posted_at }}",
              "type": "string"
            },
            {
              "id": "j6",
              "name": "score",
              "value": "={{ Math.max(0, Math.min(100, Math.round(Number($json.output.score) || 0))) }}",
              "type": "number"
            },
            {
              "id": "j7",
              "name": "fit_reasons",
              "value": "={{ $json.output.fit_reasons }}",
              "type": "string"
            },
            {
              "id": "j8",
              "name": "red_flags",
              "value": "={{ $json.output.red_flags }}",
              "type": "string"
            },
            {
              "id": "j9",
              "name": "pitch_angle",
              "value": "={{ $json.output.pitch_angle }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "m11-scored",
      "name": "Scored Job",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2200,
        160
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "g1",
              "leftValue": "={{ $json.score }}",
              "rightValue": "={{ $('Settings (edit profile)').first().json.min_score }}",
              "operator": {
                "type": "number",
                "operation": "gte"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "m11-good",
      "name": "Good Fit Only",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.2,
      "position": [
        2420,
        160
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "jobs",
        "options": {}
      },
      "id": "m11-agg",
      "name": "Collect Good Fits",
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        2640,
        160
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "r1",
              "name": "jobs",
              "value": "={{ $json.jobs.filter(j => j.title).sort((a, b) => b.score - a.score) }}",
              "type": "array"
            },
            {
              "id": "r2",
              "name": "count",
              "value": "={{ $json.jobs.filter(j => j.title).length }}",
              "type": "number"
            },
            {
              "id": "r3",
              "name": "message",
              "value": "={{ $json.jobs.filter(j => j.title).sort((a, b) => b.score - a.score).map(j => `${j.score} · ${j.title} @ ${j.company}\\n${j.pitch_angle}\\n${j.url}`).join('\\n\\n') || 'No good fits this time.' }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "m11-report",
      "name": "Build Report",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2860,
        160
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "a1",
              "leftValue": "={{ $('Scout API').isExecuted }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "m11-api",
      "name": "Called via API?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3080,
        160
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {}
      },
      "id": "m11-respond",
      "name": "Respond Report",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        3300,
        80
      ]
    },
    {
      "parameters": {},
      "id": "m11-deliver",
      "name": "Send Alert (Exercise 1)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        3300,
        260
      ]
    }
  ],
  "connections": {
    "Every 4 Hours": {
      "main": [
        [
          {
            "node": "Settings (edit profile)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scout API": {
      "main": [
        [
          {
            "node": "Settings (edit profile)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings (edit profile)": {
      "main": [
        [
          {
            "node": "Fetch RemoteOK",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch We Work Remotely",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch RemoteOK": {
      "main": [
        [
          {
            "node": "Normalize RemoteOK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch We Work Remotely": {
      "main": [
        [
          {
            "node": "Normalize WWR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize RemoteOK": {
      "main": [
        [
          {
            "node": "All Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize WWR": {
      "main": [
        [
          {
            "node": "All Jobs",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "All Jobs": {
      "main": [
        [
          {
            "node": "Matches Keywords",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Matches Keywords": {
      "main": [
        [
          {
            "node": "Newest First",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Newest First": {
      "main": [
        [
          {
            "node": "Top N",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Top N": {
      "main": [
        [
          {
            "node": "Dry Run?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Dry Run?": {
      "main": [
        [
          {
            "node": "Collect Matches",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Score Job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect Matches": {
      "main": [
        [
          {
            "node": "Respond Matches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Score Job",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Score Job": {
      "main": [
        [
          {
            "node": "Scored Job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scored Job": {
      "main": [
        [
          {
            "node": "Good Fit Only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Good Fit Only": {
      "main": [
        [
          {
            "node": "Collect Good Fits",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect Good Fits": {
      "main": [
        [
          {
            "node": "Build Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Report": {
      "main": [
        [
          {
            "node": "Called via API?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Called via API?": {
      "main": [
        [
          {
            "node": "Respond Report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Alert (Exercise 1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  }
}