Back to cookbook
EZ

Eric Zakariasson

Added ago

TypeScript ยท Intermediate

Topics to Morning Briefing

View as Markdown

This app builds a personal AI news briefing with the Grok API, a short spoken digest of what's new on the topics you follow. It's a pattern for any scheduled research job: search a fixed window, remember what's been covered, and cap what each run can spend.

Pick a few topics, and each morning it searches ๐• and the web for what happened since your last briefing, skips the stories it already told you about, and reads you the rest in about two minutes, with the posts and articles behind each story. It runs as a small web app that plays the briefing like a podcast episode while Grok researches the next topic, or in your terminal on a schedule.

What you'll learn

  • Search a fixed time window with X Search's from_date and to_date
  • Remember what each briefing covered, and let Grok decide whether a story is new, an update, or one you've heard
  • Use citations to keep only the posts and articles the searches returned
  • Show each search as it runs with the SDK's server_tool_call event
  • Cap what a run costs with the cost the API reports for every request
  • Voice each part of the briefing with client.voice.speak() while the next one is researched, with a few speech tags
  • Run it on a schedule with cron or --every

Run it

You need Node.js 22.13 or later. Put your API key in .env at the root of the repo, or export XAI_API_KEY.

Bash

cd examples/topics-to-morning-briefing/typescript
npm install
npm run web

Open http://localhost:3000 and click Brief me. The page starts with the topics in topics.txt: SpaceX, Formula 1, and AI chips. Remove one with its ร—, or type a new one and press Enter. The page works like a podcast player. Each topic shows its searches as they run, then its stories with the ๐• posts and articles beside them. The briefing starts playing as soon as the first topic is ready, highlights the topic being read, and lights up the words as they're spoken. Click Brief me again, and it tells you there's nothing new on the stories it already covered. The start screen shows what it remembers, and Forget them clears it. Click Stop or close the page to cancel a run.

To make a briefing from the terminal instead:

Bash

npm start

That briefs the topics in topics.txt, one per line, or the ones you pass, like npm start -- "SpaceX" "Formula 1". Both versions save the briefing to output/<time>/briefing.mp3 and what it covered to output/memory.json, and the terminal version writes show notes with every link to briefing.md next to the MP3.

When I ran it, a briefing on the three sample topics took one to two minutes, and up to five when the API was busy. It cost about 45 cents: about 20 cents for X Search and web search, about 20 cents for tokens, and 3 cents for speech. A run with nothing new costs about half that. X Search is billed at $5 per 1,000 posts it fetches, web search at $5 per 1,000 calls, and speech at $15 per million characters.

Run it on a schedule

Each run stops starting topics before it would go over a cost cap, $1 unless you pass --max-cost. On the page, it's the Cost cap menu. To brief yourself every morning at 7, add a line like this with crontab -e. Cron runs commands with a short PATH, so replace /path/to/node/bin with the folder that dirname "$(which node)" prints:

Text

0 7 * * * cd /path/to/xai-cookbook/examples/topics-to-morning-briefing/typescript && PATH="/path/to/node/bin:$PATH" npm start -- --max-cost 0.75 >> "$HOME/morning-briefing.log" 2>&1

Or keep it running in a terminal with --every, which briefs again that long after each briefing started, until you press Ctrl+C:

Bash

npm start -- --every 1d

How it works

The shared code is in src/briefing.ts, and the memory is in src/memory.ts. makeBriefing() researches the topics one at a time, reports each step through callbacks, and voices each topic while the next one is researched:

  1. loadMemory() reads output/memory.json, which lists each briefing's time, cost, the topics it researched, and the stories it reported. searchWindow() gives each topic a window from the day it was last covered, or from yesterday the first time, through today. X Search takes whole days in UTC, and to_date is exclusive, so the window ends tomorrow to include today. The X Search docs say to_date is included, but a search for the latest posts with to_date set to today returned none from today.
  2. researchTopic() streams one grok-4.7 request with xSearch({ from_date, to_date }) and webSearch(), and a JSON schema in text.format. The prompt lists the stories already reported on that topic in the last week and when the last briefing went out. Grok returns the stories it found, each marked new, an update, or old, along with the posts and articles behind them and what the host says about the topic. Since X Search only takes dates, Grok uses the time of the last briefing to tell what broke after it, and often narrows its own queries with ๐•'s since: operator down to the minute. The server_tool_call event reports each search as it runs. The citation event collects every URL the searches returned, and any post or article that wasn't among them is dropped. The request uses a low reasoning effort, since the searches do the work. max_turns bounds how many rounds of searches a topic can run, and idleTimeout keeps a stalled stream from holding up a scheduled run.
  3. Every response reports what it cost in usage.cost_in_usd_ticks, including the searches, and the SDK converts it to dollars in usage.cost_usd. The cost arrives only when a request is done, so a topic can't be stopped partway. Instead, a topic starts only if it would fit under the cap at the cost of the priciest topic so far. Before any topic has run, it uses the average cost of a topic in the last briefing. The outro names any topic it skipped, and the next briefing searches that topic from further back.
  4. recordClip() voices each topic's script with client.voice.speak() as soon as it's written. Grok can use a couple of speech tags, like [pause] between stories or <emphasis> on a word. The voice API reads aloud any tag it doesn't know, so the SDK's stripInvalidSpeechTags() removes those first. Now and then Grok also leaves citation markup like <citation id="web:23"/> in the script, which the voice reads as "citation ID web 23", so stripCitations() removes it. The voice API doesn't report a cost, so speech is counted at its price per character. The code writes the intro and the outro itself.
  5. Once every topic is done, the clips are joined into one MP3, and remember() adds the new stories and updates to the memory file. A run that's stopped doesn't change the memory.

src/server.ts runs makeBriefing() for the topics from the page and sends each step to it as a server-sent event. It runs one briefing at a time, since each one rewrites the memory file, and passes an AbortSignal to every request, so closing the page or clicking Stop cancels whatever is running. It serves only the audio files the run wrote, in byte ranges so the browser can seek. public/index.html is plain HTML and JavaScript that shows those events and plays the clips in order. src/index.ts prints the same steps in the terminal and writes the show notes.

To change the voice, edit VOICE in src/briefing.ts. client.voice.list() lists the built-in voices.

More from the cookbook