DEV Community

Ihor Bezruchko
Ihor Bezruchko

Posted on

I Built a Freelance Job Hunting Automation on n8n — Here's Everything I Learned

I'm a 17-year-old IT student from Luxembourg. A few months ago I got tired of spending 2-3 hours a day manually browsing Upwork, Malt, and Freelancer looking for projects.

So I built an automation system that does it for me — 24/7, on a Raspberry Pi 3.

Here's what it does, how I built it, and every painful lesson I learned along the way.


What the system does

  • Scans Upwork, Malt, and Freelancer every 30 minutes
  • Scores each job 0–100 with AI based on my profile
  • Generates proposals in English, French, and German
  • Sends the best jobs to Telegram with inline A/B buttons
  • Tracks which proposal style gets more replies
  • Sends daily stats and weekly market trend reports
  • Reminds me to follow up after 3 days

The stack

  • n8n — self-hosted workflow automation (Docker on Raspberry Pi 3)
  • Groq API (Llama 3.1-8b-instant) — AI scoring and proposal generation
  • Supabase — PostgreSQL database for jobs, proposals, clients
  • SerpAPI — searching job boards via Google
  • Apify — scraping Upwork listings
  • Telegram Bot API — alerts and bot commands
  • Cloudflare Tunnel — HTTPS for webhooks

Total running cost: ~$5/month.


7 workflows

01 - Job Discovery — runs every 30 minutes, searches 10+ sources, deduplicates via Supabase unique constraint on URL

02 - Proposal Generator — AI scores the job, generates two proposal variants (formal vs hook-first), sends to Telegram with A/B buttons

03 - Follow-up Reminders — checks Supabase every 3 days for unanswered proposals

04 - CRM via Telegram — full client management through bot commands (/jobs, /stats, /clients)

05 - Market Intelligence — daily report: how many jobs found, average score, top platforms

06 - Trend Analysis — weekly report on what skills are trending in automation

07 - Lead Generation — finds companies actively using Zapier or Make who might want to switch to n8n


Lessons learned (the hard way)

1. Cyrillic text breaks JSON body nodes silently
If you have Cyrillic characters in a JSON body field with newlines, n8n throws a "Bad control character" error. Keep everything on a single line or use \n instead of real line breaks.

2. Multiple inputs into a Code node need a Merge node first
When two HTTP Request nodes feed into one Code node, use a Merge node (Combine by Position) in between. Otherwise $('NodeName') throws "hasn't been executed" errors.

3. Cloudflare tunnel URL changes on every restart
This breaks all Telegram webhooks. I solved it with a systemd service that restarts cloudflared and updates the WEBHOOK_URL environment variable in docker-compose automatically.

4. Deduplication belongs in the database, not in n8n
Adding a UNIQUE constraint on jobs.url in Supabase and setting the insert node to "Continue on Fail" is cleaner than trying to check duplicates inside the workflow.

5. Code nodes must always return items
If a Code node returns nothing (empty array or undefined), the workflow silently passes dummy data downstream. Always return [] or a proper item.

6. LLMs ignore "return only JSON" instructions
Groq kept wrapping JSON in


json markdown blocks. Fix: strip the backticks before parsing, or use separator-based output instead of JSON.

---

## A/B testing proposals

This was the most interesting part to build. Instead of one proposal, the system now generates two variants with different tones:

- **Variant A** — formal, structured, under 100 words
- **Variant B** — hook-first, starts with the client's problem, under 70 words

Telegram sends both with buttons. When I tap which one I sent, the result gets stored in Supabase. Over time I'll have data on which style works better for which type of job.

---

## GitHub

All 7 workflow files (sanitized — API keys replaced with placeholders) are on GitHub:

👉 https://github.com/ihorbezruchko/n8n-freelance-autopilot

If you want the full working setup with the complete configuration guide:

👉 https://n8n-freelance-autopilot.netlify.app

---

Happy to answer questions about any part of the implementation. What would you build differently?
Enter fullscreen mode Exit fullscreen mode

Top comments (0)