n8n: fetch the top Hacker News stories on a schedule and save a daily digest
A workflow that fetches the five top-ranked Hacker News stories every morning and appends them to a CSV file on disk — no key, no account, no credentials. The Hacker News API is public and read-only, and the workflow file below is the exact file that was imported and run for this guide.
The workflow at a glance
Node versions in the file: Schedule Trigger 1.4, HTTP Request 4.5, Code 2, Edit Fields 3.5, Convert to File 1.2, Read/Write Files 1.1.
Key result: every morning at 8:00, the workflow fetches the top five Hacker News stories and appends them to hn-digest.csv. In the test run, two scheduled runs in a row left the file at 11 lines — a header plus the same five stories twice, with scores that had moved between runs.
Download the workflow: workflow.json — import it into n8n in step 3. It is the exact file that was imported and run for this guide, including the file path used on the test machine (change it in step 4).
What you need
- Node.js 20.19 to 24.x. n8n requires a Node.js version between 20.19 and 24.x, inclusive (n8n Docs, Install with npm). This guide ran on Node.js 24.22.1.
- n8n 2.46.7, started on your own machine. No account, no credentials — the Hacker News API is public and read-only, and its documentation says there is currently no rate limit (Hacker News API).
- A terminal for the install and the file setup.
1. Install and start n8n
Run n8n from the terminal. n8n Docs, Install with npm describes this as the quick way to try n8n locally: the command downloads what it needs and serves the editor at http://localhost:5678.
Command
npx n8n
Open http://localhost:5678. The first start asks you to create an owner account (an email address and a password, kept on your own machine). Note that npm-based installs are deprecated from n8n 3.0, per the same install page — if you are on 3.0 or later, use the Docker install it links to instead, and re-check the node versions above before importing.
2. Make the file
On a default n8n 2 install, the Read/Write Files node can only write under ~/.n8n-files. That is the default value of N8N_RESTRICT_FILE_ACCESS_TO from n8n 2.0 (n8n Docs, Read/Write Files from Disk). Create the folder and the file with its header row now — the workflow appends data rows only, so the header has to exist first.
Command
mkdir -p ~/.n8n-files
printf 'id,title,url,score\n' > ~/.n8n-files/hn-digest.csv
3. Import the workflow
In n8n, open the workflow menu (the ⋯ at the top right of the canvas) and choose Import from file, then pick workflow.json. The canvas shows eight nodes in a line. What each node does:
| Node | Type | Job |
|---|---|---|
| Every morning | Schedule Trigger | Runs the workflow at a fixed time. It works like the Cron utility: this workflow uses the custom cron expression 0 8 * * *, daily at 8:00 (n8n Docs, Schedule Trigger). You must publish the workflow for the schedule to fire. Note the timezone: the node uses the workflow timezone if set, otherwise the instance timezone, which defaults to America/New_York for self-hosted instances — set the workflow timezone in the workflow settings if 8:00 should mean your time. |
| Fetch top story IDs | HTTP Request | Method GET, URL https://hacker-news.firebaseio.com/v0/topstories.json, authentication None. The HTTP Request node makes HTTP requests to query data from any app or service with a REST API (n8n Docs, HTTP Request). The list endpoint returns story IDs, not stories — up to 500 of them, in rank order (Hacker News API). The node returns only the response body by default, and it turns each element of a JSON array into its own item (see the first trap below). |
| Take first five | Code | Keeps the first five items and turns each bare ID into an item the next node can use, in Run Once for All Items mode (n8n Docs, Code): |
Config — Take first five, JavaScript
// topstories.json returns an array of story ids, and the HTTP Request node
// turns each array element into its own item. Keep the first five.
return $input.all().slice(0, 5).map(({ json }) => ({ json: { id: json } }));
| Node | Type | Job |
|---|---|---|
| Fetch each story | HTTP Request | Method GET, URL https://hacker-news.firebaseio.com/v0/item/{{ $json.id }}.json, authentication None. It runs once per item, so it makes five requests — one per story ID. Each item lives at /v0/item/<id>.json and carries id, title, url, and score (Hacker News API). |
| Pick fields | Edit Fields | Keeps id, title, url, and score and drops the rest of the story data. Include other fields is off. The url value is {{ $json.url || 'https://news.ycombinator.com/item?id=' + $json.id }}: Ask HN posts have no url — the API docs say they carry text instead — so those rows link to the discussion page rather than landing with an empty URL. |
| Build CSV lines | Code | Turns the kept fields into CSV lines with a trailing newline, quoting any field that contains a comma, a quote, or a newline. Story titles arrive HTML-escaped, so the node decodes the common entities first: |
Config — Build CSV lines, JavaScript
// One CSV line per story, quoted where needed, ending with a newline.
// HN titles arrive HTML-escaped, so decode the common entities first.
// Convert to File's CSV mode adds a byte-order mark per chunk and no trailing
// newline, so a hand-built line is the reliable way to append rows.
const decode = (s) => String(s ?? '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/'/g, "'");
const esc = (v) => {
const s = String(v ?? '');
return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
};
const lines = $input.all().map(({ json }) =>
[json.id, decode(json.title), json.url, json.score].map(esc).join(','),
);
return [{ json: { line: lines.join('\n') + '\n' } }];
| Node | Type | Job |
|---|---|---|
| To text file | Convert to File | Operation Convert to Text File, source field line, with Add BOM off (n8n Docs, Convert to File). |
| Append to file | Read/Write Files | Operation Write File to Disk, with the Append option on (n8n Docs, Read/Write Files from Disk). |
4. Set the file path
Open the Append to file node and set File Path and Name to the absolute path of the file you made in step 2 — for example /home/alex/.n8n-files/hn-digest.csv. The downloaded file carries the path used on the test machine (/home/hatch/.n8n-files/hn-digest.csv); change it to yours. Keep the file under ~/.n8n-files unless you have changed N8N_RESTRICT_FILE_ACCESS_TO, or the write is refused.
5. Activate it
Click Publish. The schedule only fires on a published workflow — the Schedule Trigger docs say so directly (n8n Docs, Schedule Trigger). The first run lands at the next 8:00.
6. Prove the schedule works
Do not wait a day to find out whether the schedule fires. Temporarily change the schedule to run every minute, watch two runs, then set it back:
- Open the Every morning node, switch the trigger interval to Custom (Cron), and set the expression to
*/1 * * * *. - Publish the workflow again and wait about two minutes.
- Open the Executions tab. You should see two scheduled runs, both with status
success. - Set the expression back to
0 8 * * *and publish again.
In the test run for this guide, two scheduled runs fired at 15:16 and 15:17 UTC (10:16 and 10:17 in the instance timezone, America/New_York). Both finished with status success. The first added 5 rows; the second added the same five stories again, with scores that had moved — AlphaProof Code went from 288 to 290, and the Strands Decider story from 493 to 495.
7. Check the file
Command
cat ~/.n8n-files/hn-digest.csv
Output (the whole file from the test run)
id,title,url,score
50439193,AlphaProof Code,https://deepmind.google/blog/introducing-alphaproof-code/,288
50412554,NetBSD 11 desktop on a Raspberry Pi 4,https://neongd.com/articles/netbsd11-rpi4.gmi,46
50436640,Amazon saves millions by replacing Sonnet 4.6 with Strands Decider,https://strandsagents.com/blog/amazon-saves-millions-replacing-sonnet-with-strands-decider/,493
50429068,Claude Fable 5.2 and Mythos 5.2,https://www.anthropic.com/claude-fable-and-mythos-5-2,755
50437790,Queens Public library under ransomware attack e-borrowing suspended,https://connect.queenslibrary.org/,104
50439193,AlphaProof Code,https://deepmind.google/blog/introducing-alphaproof-code/,290
50412554,NetBSD 11 desktop on a Raspberry Pi 4,https://neongd.com/articles/netbsd11-rpi4.gmi,48
50436640,Amazon saves millions by replacing Sonnet 4.6 with Strands Decider,https://strandsagents.com/blog/amazon-saves-millions-replacing-sonnet-with-strands-decider/,495
50429068,Claude Fable 5.2 and Mythos 5.2,https://www.anthropic.com/claude-fable-and-mythos-5-2,756
50437790,Queens Public library under ransomware attack e-borrowing suspended,https://connect.queenslibrary.org/,104
The file held 11 lines after the test: the header plus 10 rows — the same five stories twice. Run wc -l ~/.n8n-files/hn-digest.csv after the next scheduled run — the count should grow by five each morning.
Key result: two scheduled runs, 10 rows. The first run added 5 rows; the second added the same 5 stories again, and the scores had moved between runs, which is how you know the numbers came from the live API and not a cached response. One manual run before that also succeeded; one manual run failed first on the array trap in the traps below, and the fix is in the workflow file.
Traps
| TRAP | the HTTP Request node splits a JSON array into one item per element. The first version of the Take first five node read the list as one item holding an array — $input.first().json.slice(0, 5) — and the run failed with $input.first(...).json.slice is not a function. The topstories response arrives as one item per ID (each json a bare number), so there is no array to slice. Take the first five input items instead: $input.all().slice(0, 5), as in step 3. |
|---|---|
| TRAP | Ask HN posts and job posts have no link URL. The API docs say Ask HN items carry text instead of url, and job posts often have an empty url too. A plain {{ $json.url }} mapping leaves those rows with an empty URL column. The Pick fields node falls back to the discussion page — https://news.ycombinator.com/item?id= plus the story ID — as in step 3. |
| TRAP | topstories includes job posts. The API docs say the topstories list also contains jobs. A job row still lands in the file with its title, URL, and score, exactly like a story row. If you want stories only, filter on type in the Take first five node — but then take more than five IDs first, since filtering after the cut can leave you with fewer than five rows. |
| TRAP | the header row must already exist. The workflow appends data rows only. If hn-digest.csv does not exist, the first run creates it without a header, and the file has no column names. Make the file with the header first, as in step 2. |
| TRAP | 8:00 means the instance timezone unless you set one. The Schedule Trigger uses the workflow timezone if set, otherwise the instance timezone, which defaults to America/New_York for self-hosted instances. If the workflow fires at the wrong hour, set the timezone in the workflow settings. |
| TRAP | every run re-saves the same top five. This workflow has no deduplication: each morning appends the current top five again, so the file grows by five rows a day and repeats appear. That is the point of a digest — a dated record of what was on top — but if you want only stories you have not seen before, add the seen-list pattern from the RSS guide, which keeps a list of seen items in the workflow's static data and drops repeats. |
End state
A Schedule Trigger fires daily at 8:00, an HTTP Request node fetches the top story IDs, a Code node keeps the first five, a second HTTP Request node fetches each story, Edit Fields keeps id, title, url, and score (falling back to the discussion page when a story has no URL), a second Code node builds CSV lines with HTML entities decoded, Convert to File turns them into a file, and Read/Write Files appends them. Two scheduled runs saved 10 rows — the same five stories twice, with scores that moved between runs. The one mistake that cost time during the build — treating the ID list as a single array item when the HTTP Request node splits it into one item per ID — is in the traps above, with the working fix in the workflow file.
Sources
- n8n Docs, Schedule Trigger node — Trigger Rules, the cron examples, the publish requirement, and the timezone rules
- n8n Docs, HTTP Request node — Method and URL parameters, that the node returns only the response body by default, and that it returns success only on a 2xx code
- n8n Docs, Code node — Run Once for All Items mode, and that the Code node cannot access the file system or make HTTP requests
- n8n Docs, Edit Fields (Set) node — Manual Mapping, and Keep Only Set Fields
- n8n Docs, Convert to File node — Convert to CSV and Convert to Text File operations
- n8n Docs, Read/Write Files from Disk node — Write File to Disk, the Append option, and the
~/.n8n-filesdefault in n8n 2.0 - n8n Docs, Install with npm — the
npx n8nstart, the Node.js 20.19–24.x requirement, and that npm installs are deprecated from n8n 3.0 - Hacker News API — the
topstoriesendpoint returning up to 500 IDs (also contains jobs), items at/v0/item/<id>.jsonwithid,title,url, andscore, Ask HN items carryingtextinstead ofurl, and that there is currently no rate limit
Verified 10 November 2026 on n8n 2.46.7 (Node.js 24.22.1) by importing workflow.json, publishing it, and running it on a one-minute schedule: two scheduled executions (15:16 and 15:17 UTC), both status success, mode trigger. The file held 11 lines after the test: the header plus 10 rows. Three scheduled runs at 15:12, 15:13, and 15:14 UTC also succeeded during the first test pass; the file was reset to its header before the final two-run confirmation. One manual run (15:11 UTC) succeeded first; one manual run (15:09 UTC) failed on the array trap and was fixed before the scheduled test. The schedule was restored to 0 8 * * * after the test, and the active version was checked to match the restored schedule. One limit: no title in either confirmation run contained a comma, so the CSV quoting branch was not exercised by live data in this run — the quoting code is the same pattern already proven in the webhook and RSS guides, where quoted titles appear in the shipped output.