How Stellenly works
How every job on this site is found, cleaned, read, double-checked by AI and scored, with all the rules we use to filter out noise. Job counts are live; the other figures are updated with every daily build (last: 27 Oct 2026).
1. Overview
Stellenly is a set of small, automated jobs that run on GitHub Actions. They need no server and keep no user accounts. Jobs are stored in a Postgres database (Supabase). The website itself is plain static HTML that is rebuilt every day.
- CollectRead public job APIs and employers' own job boards
- FilterKeep only English postings located in Germany (or remote)
- Clean & readStrip HTML and boilerplate; detect the German level, salary, visa support and seniority
- StoreMerge duplicates, count reposts, hide jobs that disappeared
- Validate with AIAn LLM re-reads each posting; its answer counts only with a matching quote
- CompareFind similar jobs with embeddings and turn the differences into hints
- Score & publishCalculate Hiring Scores, build pages, sitemap and feeds, deploy
| Workflow | When | What it does |
|---|---|---|
| Ingest jobs | every 6 hours | Runs the tests, then collects, filters, cleans and stores jobs and hides jobs that are gone |
| Job insights | after every ingest + daily | Embeddings, similar jobs, skills, hints, AI analysis and fact checks |
| Check dead links | daily | Re-opens up to 800 of the least recently checked postings and hides closed ones |
| Build site pages | daily | Company pages, Hiring Scores, guides, city/field pages, sitemap, RSS |
| Deploy website | after each build | Publishes the static site on GitHub Pages (HTTPS, www.stellenly.com) |
| Migrate database | on change | Applies versioned SQL migrations to the database |
| Guide review reminder | monthly | Flags guides whose fact-check date is due |
2. Where jobs come from
We only use public job feeds: no logins, no scraping behind bot protection. There are two kinds of source:
- Arbeitnow: a public job API covering Germany and Europe (up to 40 pages per run).
- Employers' own job boards, through the public APIs of their applicant-tracking systems: Greenhouse, Lever, Ashby, Personio, Recruitee, Workable and SmartRecruiters. This is the same data the company shows on its own careers page.
Automatic board discovery
Every Arbeitnow job has an "apply" link that redirects to the employer's tracking system. Once per company we follow
that redirect, recognise the system and board name from the address (for example jobs.ashbyhq.com/<company>
or <company>.jobs.personio.de), check that the board's public API answers, and save it in
ats_boards.json. From then on that company is read directly. Companies without a recognised board are tried
again after 30 days. Today 255 boards are read directly.
If the same job appears on both Arbeitnow and the employer's board, the employer's copy wins: its Apply button goes straight to the company.
| Source | Open jobs |
|---|---|
| Ashby | 678 |
| Greenhouse | 434 |
| Arbeitnow | 319 |
| Lever | 214 |
| Recruitee | 131 |
| SmartRecruiters | 102 |
| Workable | 51 |
| Personio | 27 |
Being a polite client
- Requests carry a clear User-Agent that links to the project.
- Rate limits (HTTP 429) and server errors are retried with growing waits, following
Retry-After. - There are pauses between pages, and very large SmartRecruiters boards are capped at 150 English jobs.
- If a whole source returns zero jobs, the run still saves everything else, then fails loudly so we get an email.
3. Germany & English only
Language
The title and first 3,000 characters go through a language detector (with a stop-word count as backup). Anything not detected as English is dropped, so German-language postings never appear.
Location
The location text is checked in this order:
- German place: matches a list of German cities and federal states (with and without umlauts, e.g. München / Muenchen / Munich), "Germany/Deutschland", or a "DE" country code → keep.
- Clearly abroad: a known foreign city or country → drop.
- Generic ("Remote", "EMEA", "Europe") → keep only if the job is marked remote.
- Anything else is looked up once with OpenStreetMap's Nominatim geocoder and cached (328 places cached). If it's in Germany, keep; if it can't be placed, keep only remote roles.
The city filter on the search page only lists German cities, so foreign locations never show up in the dropdown.
Other things we drop
- Postings without a title, company or link.
- ATS test postings ("TEST – …").
4. Cleaning the text
- HTML to text: tags are removed; paragraphs, line breaks and list items become clean lines and bullets; HTML entities
(
&, …) are decoded; whitespace is collapsed; text is capped at 20,000 characters. - Boilerplate removal: aggregator footers such as "Find more English Speaking Jobs in Berlin on Arbeitnow" are removed.
- Normalised names for matching: accents folded, gender suffixes like (m/w/d) removed, punctuation ignored.
- City names are normalised ("Berlin Office", "Munich HQ", "Hybrid – Hamburg" → Berlin, Munich, Hamburg).
5. Reading each posting
Every posting is read by transparent rules first. AI only corrects these rules when it can prove it (see section 7).
German requirement, our main signal
The text is checked sentence by sentence, so "Fluent English required, German is a plus" counts as nice to have, not required. Each sentence that mentions German is classified:
| Result | Example phrases |
|---|---|
| No German needed | "no German required", "you don't need to speak German", "English only" |
| Nice to have | "German is a plus", "basic German", "A2/B1", "willingness to learn German" |
| Required | "fluent/business-fluent German", "German C1", "fluent English and German", "Deutschkenntnisse" |
| Not mentioned | the posting says nothing about German |
False alarms are excluded: "strong German market", "German office", "German GAAP" or "German customers" are about Germany, not about the language. If sentences disagree, the strictest one wins (required > nice to have > not required).
Salary: only when the posting says it's pay
Showing a wrong salary is worse than showing none. So there are no estimates: a salary appears only if the posting states one.
- An amount counts only if pay words are near it (salary, Gehalt, compensation, gross, per year, OTE…) and a currency is present.
- Amounts next to funding, revenue, valuation, investors, budgets, perks, vouchers, training or equipment are ignored, unless the sentence clearly calls them salary. So "we raised €385 million" never becomes a €385k salary.
- Millions/billions ("€280M", "2 Mio") are never salaries. Hourly pay is ignored.
- Monthly pay is converted to yearly (×12). "60–80k" applies the k to both ends.
- Plausibility: €15k–€300k per year, the top of a range at most 3× the bottom. A range beats a single figure.
- Internships or student jobs above €60k are treated as parsing errors and shown as "no salary".
Right now 8% of jobs show a salary.
Visa, relocation, seniority and more
| Field | Rule |
|---|---|
| Visa sponsorship | "visa sponsorship", "visa support", "Blue Card", "work permit support"… but not when the text says "no / cannot sponsor", "must already have a work permit" or "EU citizenship required" |
| Relocation | "relocation package / support / budget", "we cover your relocation", "Umzugskosten" |
| Seniority | From the title (Intern/Werkstudent, Junior, Senior, Lead/Head/Principal/Director). Otherwise from the years of experience asked: ≤2 junior, ≤4 mid, more senior |
| Experience | The smallest "N+ years of experience" figure in the text (1–20) |
| Work setup | Hybrid ("3 days a week in the office"), Remote Germany, Remote EU ("anywhere in Europe") or on-site |
| Recruiting agency | Company names like "Recruitment", "Staffing", "Personalvermittlung", known agencies, or "on behalf of our client". Marked with a badge; the "Direct employers only" filter hides them (10 right now) |
| Blue Card badge | The stated salary meets the current EU Blue Card minimum (standard, or the reduced minimum for shortage jobs and recent graduates). This is about salary eligibility, not sponsorship |
| Tags | Field and tech keywords (Python, Product, Marketing…) plus the flags above, max 25 per job |
6. Duplicates & dead jobs
- One job, one entry: each job's ID is a fingerprint of title + company + city after normalisation, so the same job from two sources, or posted twice, is merged.
- Reposts: if a job that was gone comes back, its repost counter goes up. Frequent reposting lowers the Hiring Score.
- Gone from the source: after a complete read of a source, jobs that weren't seen any more are hidden (never deleted). Safety brake: if more than 60% of a source's jobs would disappear at once, nothing is hidden; a broken API must not empty the site.
- Dead links: every day, postings that return 404/410, redirect to a generic careers or home page, or say "position has been filled" / "no longer available" are hidden.
- Age limit: jobs posted more than 45 days ago are retired.
7. AI validation
Rules are fast and explainable, but they miss things. So a large language model (Google Gemini Flash, through an OpenAI-compatible API) re-reads postings. The AI never gets the last word without proof.
What the AI is asked
For each posting it returns JSON: a 2-sentence summary, the main tasks, up to 10 ranked skills, red flags, and the German requirement, salary, visa and relocation, each with the exact sentence it is based on.
How its answers are checked
- Quote check: an answer is kept only if its quote really appears in the posting (ignoring case and punctuation). Invented text is rejected automatically.
- German "not mentioned" is only accepted if the words German/Deutsch truly don't appear.
- Salary needs a quote with a number and must pass the same plausibility limits as the rules (stricter for internships). If the AI says the posting states no salary, we never show one.
- Visa / relocation "offered" needs a quote. "Not offered" is accepted, because rules can trip over things like "Visa card".
- Checked facts are stored in
llm_facts.jsonwith a fingerprint of the posting text. If the employer edits the posting, it is checked again.
Salary second opinion
When a salary looks wrong compared with similar jobs (section 8), the AI is shown the salary sentences and asked whether our reading is right, monthly or yearly, and whether it includes bonus or equity. Its note appears next to the warning.
Running on a free tier
- Up to 150 postings per run. Postings where the rules are least sure (German not mentioned, no salary) go first.
- Calls are paced. Rate limits and overloads are retried with exponential backoff and jitter. A used-up daily quota stops cleanly, and an unavailable model falls back to another Flash model.
- Answers wrapped in extra text or code fences are still parsed.
322 postings have AI-checked facts so far.
8. Similar jobs & hints
Each job is turned into a vector with a small open embedding model (bge-small-en-v1.5), weighted 60% title and 40%
description so the role matters more than company boilerplate. Vectors are cached between runs.
- Peers: the up to 30 most similar jobs at other companies with a cosine similarity of at least 0.75.
- Salary peers: up to 15 similar jobs that list a salary (similarity ≥ 0.72), at the same seniority level when possible. With fewer than 8 we say there isn't enough data. Otherwise we show the 25th/50th/75th percentile.
- Skills: matched against a curated list of 140 skills. Skills under "Requirements" weigh more than under "Nice to have" or "Benefits". We also show how often similar jobs ask for each skill, and which common skills this posting doesn't mention.
Hints shown on jobs
| Hint | When |
|---|---|
| 💶 Top 25% pay / Below-average pay | Salary above the 75th or below the 25th percentile of similar jobs |
| ⚠ Unusually high / low pay | Over 1.8× or under 0.55× the median of similar jobs |
| ⚠ Check salary | Student job above €70k or junior job above €120k |
| Very wide range | Top of the range more than 2.5× the bottom |
| ⚠ Experience vs level | Intern/junior role asking for 5+ years |
| Asks more experience | 4+ years more than similar jobs usually ask |
| 🇬🇧 Rare: no German | No German needed while at least half of similar jobs require it |
| 🛂 Rare: visa support | Visa support while under 10% of similar jobs offer it |
| Posted in N cities | Same role listed in 5+ cities, often a talent pool or agency |
| ⚠ Be careful | Scam signals: apply via WhatsApp/Telegram, a free email address, application fees, crypto/forex trading, high pay with "no experience needed". A negation ("we will never ask for a fee") cancels the warning. |
9. Stellenly Hiring Score
A 0–100 rating (100 points = 5 stars) of how a company hires, calculated only from its current postings on Stellenly. It is not based on employee reviews and says nothing about what it's like to work there. A company needs at least 3 open jobs to get a score. Scores are recalculated with every daily build.
| Factor | Weight | Full points when… |
|---|---|---|
| English-friendly | 25% | all jobs say German isn't needed or is only a plus. "Not mentioned" counts 60% |
| Salary transparency | 20% | 60% or more of the jobs show a salary |
| Visa & relocation | 15% | 50% or more mention visa sponsorship or relocation help |
| Hiring activity | 15% | 5 or more new jobs in the last 30 days |
| Stable postings | 15% | no reposted jobs (0 points once half are reposts) |
| Fresh postings | 10% | the median job is under 2 weeks old (0 points at 90 days) |
Each factor scales linearly between 0 and its full-points level. The total is the weighted average. Grades: Excellent ≥ 85, Very good ≥ 70, Good ≥ 55, Fair ≥ 40, otherwise Limited.
10. Views & popularity
- When someone opens a job's details or clicks Apply, the database adds 1 to that job's counter for the day. Nothing about the visitor is stored: no IP address, no cookie, no ID.
- Each browser counts a job at most once per day (remembered locally in the browser), so refreshing doesn't inflate numbers.
- Jobs show "N views · N applied". Most popular sorting uses the last 14 days, where an Apply click counts 3× an open.
- Page visits are counted with GoatCounter, a privacy-friendly, cookieless analytics service.
11. Privacy
No accounts, no cookies and no tracking pixels. Saved and applied jobs stay in your own browser. See the privacy page.