Building a Daily LinkedIn Job Newsletter with Apify + Make — Smart Keyword Scoring & Filtering

Why building this automation

If you are job hunting, manually checking LinkedIn every day and scrolling past senior roles, recruiting agency reposts, or jobs in industries you don’t want is a huge time sink. I wanted a daily email that only shows entry-level and junior roles that actually match my skill set, with weak matches and irrelevant companies or industries filtered out automatically before I ever see them.

This scenario scrapes LinkedIn jobs daily using Apify, scores each job against my resume keywords, keeps only the ones with an 80% or higher match, and emails me a clean HTML table with the company logo, title, location, salary, and exactly which keywords matched and which did not.

The Full Scenario Flow

Example Email I Receive Everyday

Step 1: Apify Run Actor (LinkedIn Jobs Scraper)

This module runs the LinkedIn Jobs Scraper actor, built by cheap_scraper on Apify. The actor supports filtering by job title, company, industry, location, and resume keyword matching directly at the scraping stage, which is what the rest of this scenario relies on.

The input is too long to fit in a single screenshot, so here is the full JSON I used.

{
    "companyExclude": [
        "DataAnnotation",
        "Turing",
        "Outlier",
        "Outlier.ai",
        "Mercor",
        "Micro1",
        "Alignerr",
        "Surge AI",
        "Invisible Technologies",
        "Remotasks",
        "Toloka",
        "Appen",
        "TELUS Digital",
        "TELUS International",
        "Lionbridge",
        "iMerit",
        "Sama",
        "TaskUs",
        "Scale AI",
        "Labelbox",
        "CloudFactory",
        "OneForma",
        "Welocalize"
    ],
    "enrichCompanyData": false,
    "excludeRecruitingAgencies": true,
    "filterEasyApply": false,
    "filterUnder10Applicants": false,
    "jobIndustryExclude": [
        "Defense and Space Manufacturing",
        "Armed Forces",
        "Military and International Affairs",
        "Banking",
        "Capital Markets",
        "Investment Banking",
        "Investment Advice",
        "Investment Management",
        "Savings Institutions",
        "Insurance Carriers",
        "Insurance Agencies and Brokerages",
        "Insurance and Employee Benefit Funds",
        "Credit Intermediation",
        "Loan Brokers",
        "Breweries",
        "Wineries",
        "Distilleries",
        "Gambling Facilities and Casinos",
        "Racetracks",
        "Tobacco Manufacturing",
        "Bars, Taverns, and Nightclubs",
        "Meat Products Manufacturing"
    ],
    "jobTitleExclude": [
        "senior",
        "sr",
        "lead",
        "principal",
        "staff",
        "manager",
        "director",
        "head",
        "architect",
        "VP",
        "chief",
        "java",
        "php",
        "c++",
        "c#",
        ".net"
    ],
    "keyword": [
        "Full Stack Developer"
    ],
    "locations": [
        "Toronto, Canada",
        "Vancouver, Canada",
        "Montreal, Canada",
        "Waterloo, Canada",
        "Ottawa, Canada",
        "Calgary, Canada"
    ],
    "publishedAt": "r86400",
    "requireRecruiterProfile": false,
    "requireSalaryInfo": false,
    "resumeKeywords": [
        {
            "aliases": [
                "TS",
                "JS",
                "JavaScript"
            ],
            "keyword": "TypeScript"
        },
        {
            "aliases": [
                "Node",
                "NodeJS"
            ],
            "keyword": "Node.js"
        },
        {
            "aliases": [
                "RN",
                "React Native",
                "Expo",
                "ReactJS"
            ],
            "keyword": "React"
        },
        {
            "aliases": [
                "PostgreSQL"
            ],
            "keyword": "SQL"
        },
        {
            "aliases": [
                "Git",
                "Docker",
                "Jira",
                "Postman",
                "GitHub",
                "GitLab",
                "GitHub Copilot",
                "Webpack"
            ],
            "keyword": "Tools"
        }
    ],
    "saveOnlyUniqueItems": true
}

A quick breakdown of the key fields:

  • The keyword field searches for a specific role, such as “Full Stack Developer.”
  • The locations field restricts results to specific cities.
  • The jobTitleExclude field removes senior, lead, staff, manager, and director titles, along with tech stacks I am not targeting, such as Java, PHP, and C++.
  • The companyExclude field filters out recruiting and BPO companies that often repost generic listings. The jobIndustryExclude field removes industries I do not want, such as defense, finance, and gambling.
  • The resumeKeywords field is the most important part. Each keyword has aliases. For example, “TypeScript” matches if the listing says “TS,” “JS,” or “JavaScript.” This avoids false negatives where a job is a strong match but uses different terminology than my resume.

Step 2: Apify Get Dataset Items

This module pulls the cleaned JSON results from the actor run so they can be processed downstream.

Step 3: Text Aggregator with Filter

This is where the matching actually happens. Only jobs scoring 80% or higher on keyword match, and flagged as a dynamic filter match, get aggregated into an HTML row. Everything else is silently dropped.

Here is the exact row template I used in the aggregator’s Text field.

<tr>
  <td>Linkedin</td>
  <td>
    <a href="{{5.applyUrl}}">{{ifempty(5.jobTitle; "N/A")}}</a>
  </td>
  <td>
    <img src="{{5.companyLogo}}" width="45" height="45" style="border-radius:6px; object-fit:contain;">
  </td>
  <td>
    <a href="{{5.companyUrl}}">{{ifempty(5.companyName; "N/A")}}</a>
  </td>
  <td>{{ifempty(5.location; "N/A")}}</td>
  <td>{{ifempty(join(5.salaryInfo; " - "); "N/A")}}</td>
  <td>{{ifempty(5.keywordMatchScorePercentage; "N/A")}}</td>
  <td>{{ifempty(join(5.matchedKeywords; "<br>"); "N/A")}}</td>
  <td>{{ifempty(join(5.unmatchedKeywords; "<br>"); "N/A")}}</td>
</tr>

Step 4: Send Email

The subject line uses this expression.

Linkedin Jobs - {{formatDate(now; "MMMM D, YYYY")}}

The HTML body wraps all the aggregated rows from Step 3 into a single table.

<table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse; width:100%; font-family:Arial, sans-serif; font-size:14px;">
  <thead>
    <tr style="background:#f2f2f2;">
      <th>Source</th>
      <th>Job Title</th>
      <th>Logo</th>
      <th>Company</th>
      <th>Location</th>
      <th>Salary</th>
      <th>Match Score</th>
      <th>Matched Keywords</th>
      <th>Unmatched Keywords</th>
    </tr>
  </thead>
  <tbody>
    {{7.text}}
  </tbody>
</table>

Here is what the final email looks like.

Key Tip

The alias trick inside resumeKeywords is what made the matching actually useful instead of noisy. Without it, you get false negatives constantly, because job postings rarely use your exact resume wording.


I also recorded a quick walkthrough of this scenario in action, in case it is helpful to see it running end to end: https://youtu.be/m8wtzKLcMEk

2 Likes