Render Tutorials
Stock research: from flaky to reliable

Baseline: Trace the request path

⏱ 8 min

You already ran research on your deployed fork. This page explains what you saw, maps the repo, and traces one request through the code. Everything still runs in a single web service.

What the app does

You type a ticker (or any research question). The backend runs four Exa web searches in parallel, then Claude writes a structured memo from the combined results.

In the UI:

  1. A text box and a submit control.
  2. Four search cards (price, news, commentary, risks). Each card moves from idle → running → done or failed.
  3. An activity log with short status lines.
  4. A memo area that fills in while Claude streams the answer.

The tasks/ folder holds search and synthesis logic, but it is compiled into the same Node process as Express for this baseline. No second service yet.

Repo layout

PathRole
ui/React UI. Posts to /api/research, listens on SSE for progress.
server/Express API and the live run registry (runner.ts).
tasks/src/queries.tsBuilds the four search queries for a ticker.
tasks/src/search.tsOne Exa search (and the simulated failure helper).
tasks/src/research.tsFans out four searches, then calls synthesis.
tasks/src/synthesize.tsClaude memo from indexed sources.
render.yamlBlueprint for the web service only.

Follow one request in the code

Open your clone on disk and trace the same path you triggered in the browser.

Browser to server

  1. The UI sends POST /api/research with a body like { "query": "TSLA" }.
  2. server/src/runner.ts startResearch(query) creates a runId, stores listeners, and starts research(query, onEvent) from tasks/src/research.ts. It does not wait for the full pipeline before responding.
  3. The HTTP response is { "runId": "…" }.
  4. The UI opens GET /api/research/:runId/events as EventSource. Each SSE message is one ResearchEvent (search card updates, log lines, memo chunks).

Inside research()

Open tasks/src/research.ts.

Fan-out. buildQueries(query) in queries.ts returns four specs: price, news, commentary, risks. The first event is { type: 'started', query, queries: [...] }.

Four searches in parallel (same Node process):

tasks/src/research.ts
const results = await Promise.all(
searches.map(async (spec, index) => {
onEvent({ type: 'search:running', index })
const result = await searchOne(query, spec, index)
onEvent({ type: 'search:done', index, articleCount: result.articles.length })
return result
}),
)

Each searchOne call hits Exa from the web service process.

Fan-in. buildIndexedArticles emits { type: 'sources', sources }. synthesize() streams the memo: synthesizing, synthesis:chunk, then { type: 'done', memo }.

On this page

  • Four parallel Exa searches, then a streamed Claude memo in one web service
  • UI driven by POST /api/research and SSE ResearchEvent messages
  • research() fans out with Promise.all, then fans in through synthesize()