Module 02 · foundations · beginner
Lesson 02 · Daily API Digest
Goal
Build a workflow that calls a public API, processes a list of results, and produces a daily digest. You'll learn the most important idea in n8n: nodes run once per item, so you rarely need loops.
Prerequisites
- Module 01 (webhooks, expressions, Respond to Webhook)
- Internet access (we call
hn.algolia.com, no API key needed)
Concepts
- HTTP Request: calls any API. The response becomes item JSON.
- Split Out: turns one item containing an array into many items, one per array element. After this, every downstream node runs per story automatically.
- Sort / Filter / Limit: list operations on items: order, keep matching, keep the first N.
- Aggregate: the opposite of Split Out. Collapses many items back into one item with an array.
- Two triggers, one flow: a Schedule Trigger for production and a Webhook for on-demand testing can feed the same nodes.
$('Node Name'): reads data from an earlier node, e.g.$('Settings').first().json.min_points.$('Node').isExecuted: tells you which trigger started this run.
Build it
Step 1: Triggers
- Schedule Trigger
Every Morning: interval Days, hour 8. - Webhook
Digest API: method GET, pathdigest, respond using a Respond to Webhook node.
Step 2: Settings
Edit Fields Settings, connected from both triggers:
min_points(Number) ={{ Number($json.query?.min_points ?? 100) }}top_n(Number) ={{ Number($json.query?.top ?? 5) }}
From the schedule there's no query, so ?? falls back to the defaults. From the webhook you can override them with ?min_points=50&top=3.
Step 3: Fetch
HTTP Request Fetch Front Page: GET https://hn.algolia.com/api/v1/search, query parameters tags=front_page, hitsPerPage=50. Run it once and inspect the output: one item with a hits array.
Step 4: Split, sort, filter, limit
- Split Out
Split Stories: fieldhits. You now have ~30 items. - Sort
Most Points First: fieldpoints, descending. - Filter
Popular Only:{{ $json.points }}(Number) is greater than or equal to{{ $('Settings').first().json.min_points }}. - Limit
Top N: max items{{ $('Settings').first().json.top_n }}.
Step 5: Shape each story
Edit Fields Format Story:
title={{ $json.title }}url={{ $json.url || 'https://news.ycombinator.com/item?id=' + $json.objectID }}(Ask HN posts have no URL)points(Number) ={{ $json.points }}comments(Number) ={{ $json.num_comments }}line=• {{ $json.title }} ({{ $json.points }} pts)
Step 6: Aggregate and summarize
- Aggregate
Build Digest: All Item Data, destination fieldstories. - Edit Fields
Add Summary:date={{ $now.toFormat('yyyy-MM-dd') }},count={{ $json.stories.length }},text={{ $json.stories.map(s => s.line).join('\n') }},stories(Array) ={{ $json.stories }}.
Step 7: Respond only when called via API
IF Called via API?: {{ $('Digest API').isExecuted }} is true.
- true → Respond to Webhook (JSON,
{{ $json }}) - false → No Operation
Deliver (Exercise 3), a placeholder for email/Slack delivery.
Publish the workflow.
Test it
./test/test.sh
Expected: 7 PASS lines (default digest ≤ 5 stories, all ≥ 100 points, sorted; custom params return exactly 3).
Common problems
- Only one item after HTTP Request: that's expected. You need Split Out on
hits. - Filter drops everything:
pointscompared as a string. Use the Number operator type, or enable Convert types where required. - Empty 200 response: no story passed the filter, so nothing reached Respond. See Exercise 2.
- Sort error "Couldn't find the field 'points'": an empty item reached Sort. Keep Sort before Filter.