Turning Hacker News into a daily podcast with ADK 2, Gemini TTS, and Cloud Run jobs | Awesome Agents on Google Cloud
Awesome Agents on Google Cloud
Turning Hacker News into a daily podcast with ADK 2, Gemini TTS, and Cloud Run jobs
A NotebookLM-style show that fact-checks every claim against its sources
I like reading Hacker News, but there is always a lot going on. I wanted a way to catch up quickly: a short daily podcast summarizing the past day’s top stories in a friendly two-host style, like NotebookLM, with fact-checking loops so that I know I can trust what I’m hearing.
So I built one. It’s an open source system that runs a Cloud Run job every morning: it reads the last 26 hours of Hacker News, picks the stories worth talking about, writes a two-host script, fact-checks it against the actual articles and comment threads, renders the audio, and publishes an episode to a podcast feed. Here is a real episode, generated end to end through the system.
The stack
Data : the Algolia HN API. No key, 10,000 requests/hour, and items/ returns a story’s full comment tree in one call. Article text is fetched with plain HTTP, with a few fallbacks in case that does not work.
Orchestration : an ADK 2 graph workflow in a Cloud Run job, triggered by Cloud Scheduler.
Models : gemini-3.1-pro-preview and gemini-3.6-flash together for generating the episode, gemini-3.1-flash-tts-preview for voice.
Publishing : a public Cloud Storage bucket with the mp3s and a generated feed.xml.
Versions at time of writing: google-adk 2.6.2, google-genai 2.17.0.
The graph
The pipeline is an ADK 2 graph workflow. The nice thing about it is that you can describe a multi-step workflow as a graph, and the structure, including the ordering, branching, and loops, stays deterministic code no matter how many model calls happen inside the nodes. To demonstrate how it works, let’s build this system from the ground up, step by step. The complete code is here.
The simplest version feeds sources to a model and writes an episode in one shot:
workflow = Workflow(<br>name="hn_digest",<br>edges=[("START", fetch_stories, write_episode)],
This declares a two-step sequence: get the raw material, then turn it into a script. fetch_stories is a plain Python function that pulls the day’s top stories from the Hacker News API. write_episode is an agent, a model call with instructions, which turns those stories into a two-host script. A tuple chain is a sequence, and each node’s return value becomes the next node’s input.
What if we want to curate which stories make the show? Separation of concerns: give curation its own node.
edges=[("START", fetch_stories, curate, write_episode)]
Same chain, one more link: fetched stories now pass through a curation step, which picks the handful worth talking about, and only those flow on to the writer.
The way curation works: the curator gets the story metadata as JSON (titles, points, comment counts) and a prompt asking it to pick 7 to 10 stories for the episode, a few as main segments and the rest as quick mentions, optimizing for variety and how interesting they are to talk about. Its output is forced into a structured list of picks, and downstream nodes process only those stories.
What about fact-checking? One more node.
edges=[("START", fetch_stories, curate, write_episode, fact_check)]
Now the finished script gets checked against the sources it came from. The checker extracts the claims from the script, then for each claim it returns a structured verdict, verified or failed, with a note explaining why. Those verdicts are the raw material for the next step.
But a single pass only finds problems. To fix them, add a router that can send the script back:
edges=[<br>("START", fetch_stories, curate, write_episode, fact_check, review),<br>(review, {"REWRITE": write_episode, "PASS": publish}),
review is a small plain function, not a model. It looks at the verdicts from fact_check. If any claim failed, it returns the route "REWRITE", and the dict in the edges sends the script back to write_episode together with the failure notes. If everything passed, it returns "PASS" and the episode moves on to publishing. review also counts the rewrites, and after two it stops sending the script back.
That is already most of the show. The production graph adds per-story work that runs in parallel, and a bounded cut path for claims that keep failing:
workflow = Workflow(<br>name="hn_digest",<br>edges=[<br>("START", fetch_candidates, curate, digest_stories, write_script,<br>fact_check, review_router),<br>(review_router, {"REWRITE": write_script,<br>"CUT": cut_failed,<br>"RENDER": render_tts}),<br>(cut_failed, fact_check),<br>(render_tts, publish),<br>],
Two things are new here. digest_stories processes every chosen story in parallel before the script is written, and the CUT route is a bounded fallback: claims that keep failing get removed from the script rather than looping forever.
How does the parallel part work? The edges you declare are fixed, but the...