<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Non-Brand Data]]></title><description><![CDATA[Non-Brand Data helps data professionals build practical judgment in ML, GenAI, and analytics through structured essays, field guides, templates, and applied workflows.]]></description><link>https://www.nb-data.com</link><image><url>https://substackcdn.com/image/fetch/$s_!06DP!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0e1cde-d120-4029-8ffd-2a8c7c6e4504_1280x1280.png</url><title>Non-Brand Data</title><link>https://www.nb-data.com</link></image><generator>Substack</generator><lastBuildDate>Fri, 11 Sep 2026 23:53:24 GMT</lastBuildDate><atom:link href="https://www.nb-data.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Cornellius Yudha Wijaya]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[cornellius@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[cornellius@substack.com]]></itunes:email><itunes:name><![CDATA[Cornellius Yudha Wijaya]]></itunes:name></itunes:owner><itunes:author><![CDATA[Cornellius Yudha Wijaya]]></itunes:author><googleplay:owner><![CDATA[cornellius@substack.com]]></googleplay:owner><googleplay:email><![CDATA[cornellius@substack.com]]></googleplay:email><googleplay:author><![CDATA[Cornellius Yudha Wijaya]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Module 1: Getting the question right before writing SQL]]></title><description><![CDATA[Learn how to define the population, metric, and time window before you write a query.]]></description><link>https://www.nb-data.com/p/module-1-getting-the-question-right</link><guid isPermaLink="false">https://www.nb-data.com/p/module-1-getting-the-question-right</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Mon, 07 Sep 2026 16:52:38 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!xDBn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc01fc7-745a-43b6-9e0b-13909f1915fc_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xDBn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc01fc7-745a-43b6-9e0b-13909f1915fc_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xDBn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc01fc7-745a-43b6-9e0b-13909f1915fc_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!xDBn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc01fc7-745a-43b6-9e0b-13909f1915fc_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!xDBn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc01fc7-745a-43b6-9e0b-13909f1915fc_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!xDBn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc01fc7-745a-43b6-9e0b-13909f1915fc_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xDBn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc01fc7-745a-43b6-9e0b-13909f1915fc_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/acc01fc7-745a-43b6-9e0b-13909f1915fc_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1183231,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/213716333?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc01fc7-745a-43b6-9e0b-13909f1915fc_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xDBn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc01fc7-745a-43b6-9e0b-13909f1915fc_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!xDBn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc01fc7-745a-43b6-9e0b-13909f1915fc_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!xDBn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc01fc7-745a-43b6-9e0b-13909f1915fc_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!xDBn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Facc01fc7-745a-43b6-9e0b-13909f1915fc_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Module 1: Getting the question right before writing SQL</figcaption></figure></div><p><strong>Module 1 of the SQL and analytics course.</strong> I&#8217;ll add the next modules as they are ready.</p><p>SQL is often used to answer business questions. However, a request such as &#8220;How are sales doing?&#8221; still leaves a few things open. We need to decide whether &#8220;sales&#8221; refers to orders, delivered orders, or revenue before we can write the query.</p><p>In my experience, the SQL itself is often not the problem. A query can run correctly and still give us the wrong business number because the population, metric, or time window was never defined.</p><p>In this module, we will learn how to turn a general request into a question we can answer with a calculation. We will use monthly sales and repeat purchase rate as examples, then see how the results change as the definition changes.</p><p>After this module, you should be able to:</p><ul><li><p>Write a business question with a clear population, metric, and time window</p></li><li><p>Choose the identifier that matches what you want to count</p></li><li><p>Check whether every row had enough time to produce the outcome you want to measure</p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h1>Before we start</h1><p>Let&#8217;s use <a href="https://www.db-fiddle.com/f/8JL5ZLxYCbWoCh2pa5x9re/2"><span>DB Fiddle</span></a> for the examples. The tables and every query in this module are already there, so you don&#8217;t need an account or a database setup. Run each query as you read, and compare the results with the explanations below them.</p><p>The examples use fifteen invented orders based on the Olist customer and order structure. The IDs, statuses, and timestamps were made for this lesson, so they are not direct Olist records. I keep the table small so we can check every row ourselves.</p><p>You can also keep the <a href="https://github.com/CornelliusYW/NBD-SQL-and-Analytic-Course/blob/main/analytics_course/01_Getting_the_question_right.sql"><span>Module 1 SQL script</span></a>.</p><div><hr></div><h1>Scenario 1: Monthly delivered orders</h1><p>Let&#8217;s start with a common question: How are sales doing? It sounds simple, but we still need to decide what sales means and which period we want to look at.</p><h2>Start with the order count</h2><p>Let&#8217;s try the first query most people would write. We count every row in the orders table.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;403e56c0-b4f1-4542-86ab-f7ec9b5b91bc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">SELECT COUNT(*) FROM orders;</code></pre></div><p>Before you scroll, try to predict the result. The query runs without errors, but can a single all-time number tell us whether sales are going up or down?</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!V2Lz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c44d45e-5d5e-4dbe-96bb-667f0d0ffb24_1219x131.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!V2Lz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c44d45e-5d5e-4dbe-96bb-667f0d0ffb24_1219x131.png 424w, https://substackcdn.com/image/fetch/$s_!V2Lz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c44d45e-5d5e-4dbe-96bb-667f0d0ffb24_1219x131.png 848w, https://substackcdn.com/image/fetch/$s_!V2Lz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c44d45e-5d5e-4dbe-96bb-667f0d0ffb24_1219x131.png 1272w, https://substackcdn.com/image/fetch/$s_!V2Lz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c44d45e-5d5e-4dbe-96bb-667f0d0ffb24_1219x131.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!V2Lz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c44d45e-5d5e-4dbe-96bb-667f0d0ffb24_1219x131.png" width="1219" height="131" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5c44d45e-5d5e-4dbe-96bb-667f0d0ffb24_1219x131.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:131,&quot;width&quot;:1219,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Title: DB Fiddle result showing a total order count of 15 - Description: DB Fiddle result showing a total order count of 15&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Title: DB Fiddle result showing a total order count of 15 - Description: DB Fiddle result showing a total order count of 15" title="Title: DB Fiddle result showing a total order count of 15 - Description: DB Fiddle result showing a total order count of 15" srcset="https://substackcdn.com/image/fetch/$s_!V2Lz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c44d45e-5d5e-4dbe-96bb-667f0d0ffb24_1219x131.png 424w, https://substackcdn.com/image/fetch/$s_!V2Lz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c44d45e-5d5e-4dbe-96bb-667f0d0ffb24_1219x131.png 848w, https://substackcdn.com/image/fetch/$s_!V2Lz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c44d45e-5d5e-4dbe-96bb-667f0d0ffb24_1219x131.png 1272w, https://substackcdn.com/image/fetch/$s_!V2Lz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c44d45e-5d5e-4dbe-96bb-667f0d0ffb24_1219x131.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>As we can see from the result, the table has fifteen orders. The query is correct, but we still don&#8217;t know how sales are doing.</p><div><hr></div><h2>What is still missing</h2><p>There are three things we still need to decide:</p><ol><li><p><strong>Which orders should we count?</strong> The table has orders delivered, cancelled, shipped, and invoiced. We need to decide which of them should count as sales.</p></li><li><p><strong>What should sales measure?</strong> We could count orders, add revenue, or count items sold. These numbers can move differently.</p></li><li><p><strong>Which period should we use?</strong> One all-time total can&#8217;t show whether the latest month is higher or lower than the previous month.</p></li></ol><p>This happens because the request doesn&#8217;t define the population, metric, or time window. If we make those choices ourselves, another analyst can make different choices and return a different number. Both queries might still be valid.</p><div><hr></div><h2>Define the question</h2><p>Before we write more SQL, let&#8217;s write the question and metric first:</p><ul><li><p><strong><span>Question:</span></strong><span> How has the number of delivered orders changed month-by-month across 2017 and 2018?</span></p></li><li><p><strong><span>Metric:</span></strong><span> Count one order when its status is delivered, then group the count by the month in which the order was placed.</span></p></li></ul><p>For me, delivered orders are a good starting point because we want completed sales rather than every order attempt. I would calculate revenue separately because refunds and payment timing need their own rules.</p><div><hr></div><h2>Calculate monthly delivered orders</h2><p>We can use the following query:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;4d77ed2a-2339-40a6-99ca-4fb8bc822c79&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">SELECT
 date_trunc(&#8217;month&#8217;, order_purchase_timestamp) AS order_month,
 COUNT(*) AS delivered_orders
FROM orders
WHERE order_status = &#8216;delivered&#8217;
 AND order_purchase_timestamp &gt;= &#8216;2017-01-01&#8217;
 AND order_purchase_timestamp &lt; &#8216;2019-01-01&#8217;
GROUP BY 1
ORDER BY 1;</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DRcO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72ca46ee-7c13-4a6b-801a-08e14da48eb6_1219x392.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DRcO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72ca46ee-7c13-4a6b-801a-08e14da48eb6_1219x392.png 424w, https://substackcdn.com/image/fetch/$s_!DRcO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72ca46ee-7c13-4a6b-801a-08e14da48eb6_1219x392.png 848w, https://substackcdn.com/image/fetch/$s_!DRcO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72ca46ee-7c13-4a6b-801a-08e14da48eb6_1219x392.png 1272w, https://substackcdn.com/image/fetch/$s_!DRcO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72ca46ee-7c13-4a6b-801a-08e14da48eb6_1219x392.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DRcO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72ca46ee-7c13-4a6b-801a-08e14da48eb6_1219x392.png" width="1219" height="392" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/72ca46ee-7c13-4a6b-801a-08e14da48eb6_1219x392.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:392,&quot;width&quot;:1219,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Title: DB Fiddle result showing seven months of delivered-order counts - Description: DB Fiddle result showing seven months of delivered-order counts&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Title: DB Fiddle result showing seven months of delivered-order counts - Description: DB Fiddle result showing seven months of delivered-order counts" title="Title: DB Fiddle result showing seven months of delivered-order counts - Description: DB Fiddle result showing seven months of delivered-order counts" srcset="https://substackcdn.com/image/fetch/$s_!DRcO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72ca46ee-7c13-4a6b-801a-08e14da48eb6_1219x392.png 424w, https://substackcdn.com/image/fetch/$s_!DRcO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72ca46ee-7c13-4a6b-801a-08e14da48eb6_1219x392.png 848w, https://substackcdn.com/image/fetch/$s_!DRcO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72ca46ee-7c13-4a6b-801a-08e14da48eb6_1219x392.png 1272w, https://substackcdn.com/image/fetch/$s_!DRcO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F72ca46ee-7c13-4a6b-801a-08e14da48eb6_1219x392.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>As we can see from the result, we now have a delivered-order count for each month available in the teaching data. Instead of a single total, we have seven rows to compare.</p><p><code>date_trunc(&#8217;month&#8217;, ...) </code>changes each timestamp to the first day of its month. For example, both 2017-03-02 and 2017-03-22 are treated as 2017-03-01, so they are counted in the same group. <code>GROUP BY 1 </code>refers to the first selected column, so we don&#8217;t need to repeat the full expression.</p><p>We also use a half-open date range. It includes timestamps on or after 1 January 2017 and stops before 1 January 2019. If we use <span>&lt;= &#8216;2018-12-31&#8217;</span> instead, an order placed later on 31 December could be left out because the column also contains a time.</p><p>There are only seven months in the result because the teaching data doesn&#8217;t cover the full two years. We need to check the available dates before we describe the trend.</p><div><hr></div><h2>What changed</h2><ul><li><p>We count delivered orders instead of every order status.</p></li><li><p>We calculate a value per month rather than an all-time total.</p></li><li><p>The question and the query now use the same definition.</p></li></ul><p><strong>Your call.</strong> Delivered orders are not always the right sales metric. You can use all orders or revenue if that matches the question better. The important thing is to write the choice down before calculating the result.</p><p><strong>When to break the rule.</strong> During early exploration, you might not know the final question yet. That&#8217;s fine. Just make it clear that you are exploring and not presenting the result as the final answer.</p><p><strong>What to say.</strong> &#8220;Before I run this, should sales mean orders or revenue, should I include only delivered orders, and which period do you want to compare?&#8221;</p><p>At this point, you should be able to:</p><ul><li><p>Explain why an order count doesn&#8217;t answer every sales question</p></li><li><p>Define the population, metric, and time window</p></li><li><p>Compare the written definition with the query</p></li></ul><div><hr></div><h1>Scenario 2: Repeat purchase rate</h1><p>Next, let&#8217;s look at repeat purchase rate. The metric already has a name, but that doesn&#8217;t mean the definition is complete. We still need to decide what counts as a single customer and how long we give someone before they need to order again.</p><h2>Start with Customer ID</h2><p>Let&#8217;s begin by counting orders for each <code>customer_id</code>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;a84af112-cb65-4c27-a234-67f84d971b4e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">SELECT
 ROUND(100.0 * COUNT(*) FILTER (WHERE order_count &gt; 1) / COUNT(*), 1) AS repeat_rate_pct
FROM (
 SELECT customer_id, COUNT(*) AS order_count
 FROM orders
 GROUP BY customer_id
) c;</code></pre></div><p>Before you scroll, try to predict the result. What would you think if the repeat purchase rate came back as exactly zero?</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6duq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9dfef8a-4549-47f8-8d95-cb0c7cff86fd_1219x131.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6duq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9dfef8a-4549-47f8-8d95-cb0c7cff86fd_1219x131.png 424w, https://substackcdn.com/image/fetch/$s_!6duq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9dfef8a-4549-47f8-8d95-cb0c7cff86fd_1219x131.png 848w, https://substackcdn.com/image/fetch/$s_!6duq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9dfef8a-4549-47f8-8d95-cb0c7cff86fd_1219x131.png 1272w, https://substackcdn.com/image/fetch/$s_!6duq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9dfef8a-4549-47f8-8d95-cb0c7cff86fd_1219x131.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6duq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9dfef8a-4549-47f8-8d95-cb0c7cff86fd_1219x131.png" width="1219" height="131" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e9dfef8a-4549-47f8-8d95-cb0c7cff86fd_1219x131.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:131,&quot;width&quot;:1219,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Title: DB Fiddle result showing a repeat rate of 0.0 percent - Description: DB Fiddle result showing a repeat rate of 0.0 percent&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Title: DB Fiddle result showing a repeat rate of 0.0 percent - Description: DB Fiddle result showing a repeat rate of 0.0 percent" title="Title: DB Fiddle result showing a repeat rate of 0.0 percent - Description: DB Fiddle result showing a repeat rate of 0.0 percent" srcset="https://substackcdn.com/image/fetch/$s_!6duq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9dfef8a-4549-47f8-8d95-cb0c7cff86fd_1219x131.png 424w, https://substackcdn.com/image/fetch/$s_!6duq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9dfef8a-4549-47f8-8d95-cb0c7cff86fd_1219x131.png 848w, https://substackcdn.com/image/fetch/$s_!6duq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9dfef8a-4549-47f8-8d95-cb0c7cff86fd_1219x131.png 1272w, https://substackcdn.com/image/fetch/$s_!6duq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9dfef8a-4549-47f8-8d95-cb0c7cff86fd_1219x131.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>As we can see from the result, the repeat rate is 0.0%. This doesn&#8217;t mean nobody ordered twice. It means <code>customer_id</code> is not the right identifier for this metric.</p><div><hr></div><h2>Check the customer identifier</h2><p>In the Olist data model, each order gets a new <code>customer_id</code>. The teaching data follows the same structure, so 15 orders also yield 15 distinct <code>customer_id </code>values.</p><p>Because each <code>customer_id</code> appears once, this query can never find a repeat customer. To track the same person across multiple orders, we need the <code>customer_unique_id</code>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WOgR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a90676f-b649-4b90-b712-075bd3170f33_1219x686.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WOgR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a90676f-b649-4b90-b712-075bd3170f33_1219x686.png 424w, https://substackcdn.com/image/fetch/$s_!WOgR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a90676f-b649-4b90-b712-075bd3170f33_1219x686.png 848w, https://substackcdn.com/image/fetch/$s_!WOgR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a90676f-b649-4b90-b712-075bd3170f33_1219x686.png 1272w, https://substackcdn.com/image/fetch/$s_!WOgR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a90676f-b649-4b90-b712-075bd3170f33_1219x686.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WOgR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a90676f-b649-4b90-b712-075bd3170f33_1219x686.png" width="1219" height="686" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1a90676f-b649-4b90-b712-075bd3170f33_1219x686.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:686,&quot;width&quot;:1219,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Title: Customer identity diagram showing several order-level customer IDs belonging to one customer_unique_id - Description: Customer identity diagram showing several order-level customer IDs belonging to one customer_unique_id&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Title: Customer identity diagram showing several order-level customer IDs belonging to one customer_unique_id - Description: Customer identity diagram showing several order-level customer IDs belonging to one customer_unique_id" title="Title: Customer identity diagram showing several order-level customer IDs belonging to one customer_unique_id - Description: Customer identity diagram showing several order-level customer IDs belonging to one customer_unique_id" srcset="https://substackcdn.com/image/fetch/$s_!WOgR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a90676f-b649-4b90-b712-075bd3170f33_1219x686.png 424w, https://substackcdn.com/image/fetch/$s_!WOgR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a90676f-b649-4b90-b712-075bd3170f33_1219x686.png 848w, https://substackcdn.com/image/fetch/$s_!WOgR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a90676f-b649-4b90-b712-075bd3170f33_1219x686.png 1272w, https://substackcdn.com/image/fetch/$s_!WOgR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1a90676f-b649-4b90-b712-075bd3170f33_1219x686.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The diagram shows the difference between the two columns. The <code>customer_id </code>identifies the customer record for a single order, while <code>customer_unique_id</code> links multiple order records to the same person in the teaching data.</p><p>However, the customer identifier is not the only factor in our decision. We also need to choose the repeat window and decide whether an order must be delivered before it counts as a repeat purchase.</p><div><hr></div><h2>Define the repeat purchase rate</h2><p>Let&#8217;s use this definition:</p><p><strong><span>Metric:</span></strong><span> Among customers who placed a delivered order, calculate the percentage whose next delivered order was placed within 90 days. Count one customer per </span><code>customer_unique_id</code><span>.</span></p><p>Now we have the customer key, order status, and time window. We can use those choices to decide which SQL functions we need.</p><div><hr></div><h2>Calculate the first repeat rate</h2><p>The following query uses CTEs and window functions. We will cover this in Modules 10 and 11, so you don&#8217;t need to write it from scratch yet. For now, look at how each part of the query matches the metric definition.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;d12c5643-d8ef-4c61-ad12-80d509b8e4af&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">WITH delivered_orders AS (
 SELECT
 c.customer_unique_id,
 o.order_purchase_timestamp
 FROM orders o
 JOIN customers c ON c.customer_id = o.customer_id
 WHERE o.order_status = &#8216;delivered&#8217;
),
sequenced AS (
 SELECT
 customer_unique_id,
 order_purchase_timestamp,
 ROW_NUMBER() OVER (
 PARTITION BY customer_unique_id
 ORDER BY order_purchase_timestamp
 ) AS order_seq,
 LEAD(order_purchase_timestamp) OVER (
 PARTITION BY customer_unique_id
 ORDER BY order_purchase_timestamp
 ) AS next_order_ts
 FROM delivered_orders
)
SELECT
 ROUND(
 100.0 * COUNT(*) FILTER (
 WHERE next_order_ts IS NOT NULL
 AND next_order_ts &lt;= order_purchase_timestamp + INTERVAL &#8216;90 days&#8217;
 ) / COUNT(*)
 , 1) AS repeat_rate_pct
FROM sequenced
WHERE order_seq = 1;</code></pre></div><p>The first CTE keeps only delivered orders and joins them to <code>customer_unique_id</code>. The second CTE puts each customer&#8217;s orders in time order. Then <code>LEAD()</code> finds the next delivered order for the same customer.</p><p>Before you scroll, what do you expect now? Should the repeat rate stay at zero or increase?</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xBIF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe474c2d1-f707-4f92-97bb-d4526663acae_1219x104.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xBIF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe474c2d1-f707-4f92-97bb-d4526663acae_1219x104.png 424w, https://substackcdn.com/image/fetch/$s_!xBIF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe474c2d1-f707-4f92-97bb-d4526663acae_1219x104.png 848w, https://substackcdn.com/image/fetch/$s_!xBIF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe474c2d1-f707-4f92-97bb-d4526663acae_1219x104.png 1272w, https://substackcdn.com/image/fetch/$s_!xBIF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe474c2d1-f707-4f92-97bb-d4526663acae_1219x104.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xBIF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe474c2d1-f707-4f92-97bb-d4526663acae_1219x104.png" width="1219" height="104" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e474c2d1-f707-4f92-97bb-d4526663acae_1219x104.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:104,&quot;width&quot;:1219,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Title: DB Fiddle result showing a 20.0 percent repeat rate before the full 90-day eligibility check - Description: DB Fiddle result showing a 20.0 percent repeat rate before the full 90-day eligibility check&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Title: DB Fiddle result showing a 20.0 percent repeat rate before the full 90-day eligibility check - Description: DB Fiddle result showing a 20.0 percent repeat rate before the full 90-day eligibility check" title="Title: DB Fiddle result showing a 20.0 percent repeat rate before the full 90-day eligibility check - Description: DB Fiddle result showing a 20.0 percent repeat rate before the full 90-day eligibility check" srcset="https://substackcdn.com/image/fetch/$s_!xBIF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe474c2d1-f707-4f92-97bb-d4526663acae_1219x104.png 424w, https://substackcdn.com/image/fetch/$s_!xBIF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe474c2d1-f707-4f92-97bb-d4526663acae_1219x104.png 848w, https://substackcdn.com/image/fetch/$s_!xBIF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe474c2d1-f707-4f92-97bb-d4526663acae_1219x104.png 1272w, https://substackcdn.com/image/fetch/$s_!xBIF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe474c2d1-f707-4f92-97bb-d4526663acae_1219x104.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Compared with the previous result, the rate increases from 0.0% to 20.0%. We fixed the customer identifier, but there is still one problem with the denominator.</p><div><hr></div><h2>Check the 90-day window</h2><p>To measure a 90-day repeat rate, every customer in the denominator needs 90 days to place another order. A customer who first ordered near the end of the data doesn&#8217;t have the same chance as someone who ordered months earlier.</p><p>Let&#8217;s check how many days each customer had after the first delivered order:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;90039bfe-0dc4-4058-827b-13828927adeb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">SELECT
 customer_unique_id,
 order_purchase_timestamp AS first_order,
 DATE_PART(&#8217;day&#8217;, (SELECT MAX(order_purchase_timestamp) FROM orders)
 - order_purchase_timestamp) AS days_they_had
FROM sequenced
WHERE order_seq = 1
ORDER BY days_they_had;</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Oo5b!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67bd5df6-ceff-42c0-826b-ad6f9d290b56_1219x209.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Oo5b!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67bd5df6-ceff-42c0-826b-ad6f9d290b56_1219x209.png 424w, https://substackcdn.com/image/fetch/$s_!Oo5b!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67bd5df6-ceff-42c0-826b-ad6f9d290b56_1219x209.png 848w, https://substackcdn.com/image/fetch/$s_!Oo5b!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67bd5df6-ceff-42c0-826b-ad6f9d290b56_1219x209.png 1272w, https://substackcdn.com/image/fetch/$s_!Oo5b!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67bd5df6-ceff-42c0-826b-ad6f9d290b56_1219x209.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Oo5b!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67bd5df6-ceff-42c0-826b-ad6f9d290b56_1219x209.png" width="1219" height="209" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/67bd5df6-ceff-42c0-826b-ad6f9d290b56_1219x209.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:209,&quot;width&quot;:1219,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Title: DB Fiddle result showing customers with 0, 35 and 40 days available to return - Description: DB Fiddle result showing customers with 0, 35 and 40 days available to return&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Title: DB Fiddle result showing customers with 0, 35 and 40 days available to return - Description: DB Fiddle result showing customers with 0, 35 and 40 days available to return" title="Title: DB Fiddle result showing customers with 0, 35 and 40 days available to return - Description: DB Fiddle result showing customers with 0, 35 and 40 days available to return" srcset="https://substackcdn.com/image/fetch/$s_!Oo5b!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67bd5df6-ceff-42c0-826b-ad6f9d290b56_1219x209.png 424w, https://substackcdn.com/image/fetch/$s_!Oo5b!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67bd5df6-ceff-42c0-826b-ad6f9d290b56_1219x209.png 848w, https://substackcdn.com/image/fetch/$s_!Oo5b!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67bd5df6-ceff-42c0-826b-ad6f9d290b56_1219x209.png 1272w, https://substackcdn.com/image/fetch/$s_!Oo5b!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67bd5df6-ceff-42c0-826b-ad6f9d290b56_1219x209.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>As we can see from the result, one customer first ordered on the final day in the data. Two more customers had only 35 or 40 days. The current denominator counts all three as customers who didn't return, even though we can't observe a full 90-day outcome for them.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WmaM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84954659-82fc-4f6f-9ad3-8e00eb75cd20_1600x900.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WmaM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84954659-82fc-4f6f-9ad3-8e00eb75cd20_1600x900.png 424w, https://substackcdn.com/image/fetch/$s_!WmaM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84954659-82fc-4f6f-9ad3-8e00eb75cd20_1600x900.png 848w, https://substackcdn.com/image/fetch/$s_!WmaM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84954659-82fc-4f6f-9ad3-8e00eb75cd20_1600x900.png 1272w, https://substackcdn.com/image/fetch/$s_!WmaM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84954659-82fc-4f6f-9ad3-8e00eb75cd20_1600x900.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WmaM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84954659-82fc-4f6f-9ad3-8e00eb75cd20_1600x900.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/84954659-82fc-4f6f-9ad3-8e00eb75cd20_1600x900.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Updated 90-day eligibility diagram&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Updated 90-day eligibility diagram" title="Updated 90-day eligibility diagram" srcset="https://substackcdn.com/image/fetch/$s_!WmaM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84954659-82fc-4f6f-9ad3-8e00eb75cd20_1600x900.png 424w, https://substackcdn.com/image/fetch/$s_!WmaM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84954659-82fc-4f6f-9ad3-8e00eb75cd20_1600x900.png 848w, https://substackcdn.com/image/fetch/$s_!WmaM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84954659-82fc-4f6f-9ad3-8e00eb75cd20_1600x900.png 1272w, https://substackcdn.com/image/fetch/$s_!WmaM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84954659-82fc-4f6f-9ad3-8e00eb75cd20_1600x900.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The diagram compares the 90-day window to the end of the data period. Seven customers have a complete observation window, while three need to be removed from the denominator.</p><h2>Calculate the corrected rate</h2><p>We can fix the denominator by keeping only customers whose first delivered order was at least 90 days before the final timestamp:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;181d792a-bebb-4712-98fd-1f059c73868f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">WITH delivered_orders AS (
 SELECT
 c.customer_unique_id,
 o.order_purchase_timestamp
 FROM orders o
 JOIN customers c ON c.customer_id = o.customer_id
 WHERE o.order_status = &#8216;delivered&#8217;
),
sequenced AS (
 SELECT
 customer_unique_id,
 order_purchase_timestamp,
 ROW_NUMBER() OVER (
 PARTITION BY customer_unique_id
 ORDER BY order_purchase_timestamp
 ) AS order_seq,
 LEAD(order_purchase_timestamp) OVER (
 PARTITION BY customer_unique_id
 ORDER BY order_purchase_timestamp
 ) AS next_order_ts
 FROM delivered_orders
)
SELECT
 ROUND(
 100.0 * COUNT(*) FILTER (
 WHERE next_order_ts IS NOT NULL
 AND next_order_ts &lt;= order_purchase_timestamp + INTERVAL &#8216;90 days&#8217;
 ) / COUNT(*)
 , 1) AS repeat_rate_pct,
 COUNT(*) AS customers_counted
FROM sequenced
WHERE order_seq = 1
 AND order_purchase_timestamp + INTERVAL &#8216;90 days&#8217;
 &lt;= (SELECT MAX(order_purchase_timestamp) FROM orders);</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-fFl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1560927-dbd7-4c61-941e-f32868188f47_1219x104.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-fFl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1560927-dbd7-4c61-941e-f32868188f47_1219x104.png 424w, https://substackcdn.com/image/fetch/$s_!-fFl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1560927-dbd7-4c61-941e-f32868188f47_1219x104.png 848w, https://substackcdn.com/image/fetch/$s_!-fFl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1560927-dbd7-4c61-941e-f32868188f47_1219x104.png 1272w, https://substackcdn.com/image/fetch/$s_!-fFl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1560927-dbd7-4c61-941e-f32868188f47_1219x104.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-fFl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1560927-dbd7-4c61-941e-f32868188f47_1219x104.png" width="1219" height="104" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d1560927-dbd7-4c61-941e-f32868188f47_1219x104.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:104,&quot;width&quot;:1219,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Title: DB Fiddle result showing a corrected 28.6 percent repeat rate across seven eligible customers - Description: DB Fiddle result showing a corrected 28.6 percent repeat rate across seven eligible customers&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Title: DB Fiddle result showing a corrected 28.6 percent repeat rate across seven eligible customers - Description: DB Fiddle result showing a corrected 28.6 percent repeat rate across seven eligible customers" title="Title: DB Fiddle result showing a corrected 28.6 percent repeat rate across seven eligible customers - Description: DB Fiddle result showing a corrected 28.6 percent repeat rate across seven eligible customers" srcset="https://substackcdn.com/image/fetch/$s_!-fFl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1560927-dbd7-4c61-941e-f32868188f47_1219x104.png 424w, https://substackcdn.com/image/fetch/$s_!-fFl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1560927-dbd7-4c61-941e-f32868188f47_1219x104.png 848w, https://substackcdn.com/image/fetch/$s_!-fFl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1560927-dbd7-4c61-941e-f32868188f47_1219x104.png 1272w, https://substackcdn.com/image/fetch/$s_!-fFl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1560927-dbd7-4c61-941e-f32868188f47_1219x104.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The result changes from 20.0% to 28.6%. The first query counted 10 customers, while the corrected one counts 7 customers who had a full 90-day window.</p><p>Now we can write the final definition:</p><p><strong><span>Metric:</span></strong><span> Among customers whose first delivered order was placed at least 90 days before the data ends, calculate the percentage who placed another delivered order within 90 days. Count one customer by </span><code>customer_unique_id.</code></p><div><hr></div><h2>What changed</h2><ul><li><p>We changed the customer key from <code>customer_id</code> to <code>customer_unique_id</code>.</p></li><li><p>We count only delivered orders before checking for a repeat purchase.</p></li><li><p>We remove customers who don&#8217;t have the full 90 days.</p></li></ul><p><strong>Your call.</strong> You still need to decide what one customer means, how long the repeat window should be, and what to do with customers who don&#8217;t have the full window. It depends on how the business defines repeat purchasing.</p><p><strong>When to break the rule.</strong> If only a few customers have incomplete windows and the rate barely changes, you might keep them and state the limitation. Check the difference first.</p><p><strong>What to say.</strong> &#8220;I counted customers by <code>customer_unique_id</code>, included delivered orders, and removed customers who didn&#8217;t have a full 90 days to return.&#8221;</p><p><strong>In an interview.</strong> If someone asks how you would measure repeat purchase rate, I wouldn&#8217;t start with the window function. I would first define one customer, the qualifying order status, the repeat window, and the observation period. Then I would explain how the query uses each choice.</p><p>At this point, you should be able to:</p><ul><li><p><span>C</span>hoose the customer identifier that matches the metric</p></li><li><p>Make the numerator and denominator follow the same definition</p></li><li><p>Check whether the available data covers the full measurement window</p></li></ul><div><hr></div><h1>Manager&#8217;s view</h1><p>When I review an analysis, I would ask for the written question and metric first. That makes it easier to compare the query with what the analyst actually meant to calculate.</p><p>For a rate, I would also check the customer key and the time window. If recent customers are counted as failures before they have a chance to return, the rate will be lower than it should be under that definition.</p><div><hr></div><h1>Try it</h1><p>On the teaching data. Take the request &#8220;Which customers are doing well?&#8221; and turn it into a question with a population, metric, and time window. Then write what needs to be true about the customers in the denominator.</p><p>On your own data. Choose a real request from your work or a dataset you care about. Write the question and metric in the same format, then write one assumption you need to check before calculating it.</p><p>Keep the second exercise. It becomes the first page of the analysis you will build through this course.</p><div><hr></div><h1>Keep this</h1><p>Use the Question and Metric Brief to write down the question, population, metric, time window, decision, and fairness check before opening the SQL editor.</p><p><span>Download the PDF</span> below:</p><div class="file-embed-wrapper" data-component-name="FileToDOM"><div class="file-embed-container-reader"><div class="file-embed-container-top"><image class="file-embed-thumbnail-default" src="https://substackcdn.com/image/fetch/$s_!0Cy0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Fimg%2Fattachment_icon.svg"></image><div class="file-embed-details"><div class="file-embed-details-h1">Question And Metric Brief</div><div class="file-embed-details-h2">39.4KB &#8729; PDF file</div></div><a class="file-embed-button wide" href="https://www.nb-data.com/api/v1/file/e9f67d65-6164-42a4-b7aa-05a20747adb8.pdf"><span class="file-embed-button-text">Download</span></a></div><a class="file-embed-button narrow" href="https://www.nb-data.com/api/v1/file/e9f67d65-6164-42a4-b7aa-05a20747adb8.pdf"><span class="file-embed-button-text">Download</span></a></div></div><div><hr></div><h1>Mark this module complete</h1><p>Complete one Try-it exercise and save the Question and Metric Brief. Keep it with the SQL and report you build through the rest of the course.</p><p>This module counts toward the Certificate of Completion once you complete all 14 modules and their activities. The separate Verified credential requires a finished project and a review against the published rubric.</p><h1>Next</h1><p><span>Module 2: SELECT and FROM: reading the table</span> looks at what one row represents before we count it.</p><p>Subscribe to receive the next module when it is published.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[SQL and Analytics course]]></title><description><![CDATA[Certificate Available! Learn how to define metrics, check your analysis, and explain the result in the era of AI.]]></description><link>https://www.nb-data.com/p/sql-and-analytics-course</link><guid isPermaLink="false">https://www.nb-data.com/p/sql-and-analytics-course</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Mon, 07 Sep 2026 16:52:22 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Z5Ws!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1acf707f-f6ee-441e-88fc-c83c05268a6a_1194x560.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Z5Ws!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1acf707f-f6ee-441e-88fc-c83c05268a6a_1194x560.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Z5Ws!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1acf707f-f6ee-441e-88fc-c83c05268a6a_1194x560.png 424w, https://substackcdn.com/image/fetch/$s_!Z5Ws!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1acf707f-f6ee-441e-88fc-c83c05268a6a_1194x560.png 848w, https://substackcdn.com/image/fetch/$s_!Z5Ws!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1acf707f-f6ee-441e-88fc-c83c05268a6a_1194x560.png 1272w, https://substackcdn.com/image/fetch/$s_!Z5Ws!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1acf707f-f6ee-441e-88fc-c83c05268a6a_1194x560.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Z5Ws!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1acf707f-f6ee-441e-88fc-c83c05268a6a_1194x560.png" width="1194" height="560" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1acf707f-f6ee-441e-88fc-c83c05268a6a_1194x560.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:560,&quot;width&quot;:1194,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:562317,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/213569114?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1acf707f-f6ee-441e-88fc-c83c05268a6a_1194x560.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Z5Ws!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1acf707f-f6ee-441e-88fc-c83c05268a6a_1194x560.png 424w, https://substackcdn.com/image/fetch/$s_!Z5Ws!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1acf707f-f6ee-441e-88fc-c83c05268a6a_1194x560.png 848w, https://substackcdn.com/image/fetch/$s_!Z5Ws!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1acf707f-f6ee-441e-88fc-c83c05268a6a_1194x560.png 1272w, https://substackcdn.com/image/fetch/$s_!Z5Ws!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1acf707f-f6ee-441e-88fc-c83c05268a6a_1194x560.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>SQL is one of the most common skills you need for a data role. Most of us start by learning <code>SELECT</code>, <code>WHERE</code>, and how to join tables. These are important, but knowing the syntax doesn&#8217;t always mean the answer is right.</p><p>For example, a SQL query can run without errors and still return the wrong number. In my experience, this is where SQL work becomes harder. You need to decide what to count, check if the result makes sense, and explain why you did it that way.</p><p>That is what I want to focus on in this course. There are fourteen short modules, and each one presents a SQL problem where the query may look correct but still produce the wrong answer.</p><p><strong>Module 1 is free and available now.</strong> You can run every query directly without installing anything.</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/sql-and-analytics-course?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Thanks for reading Non-Brand Data! This post is public so feel free to share it.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/sql-and-analytics-course?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/sql-and-analytics-course?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6VtD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cc37f6e-1bb3-4781-9de0-edf018fbb75f_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6VtD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cc37f6e-1bb3-4781-9de0-edf018fbb75f_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!6VtD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cc37f6e-1bb3-4781-9de0-edf018fbb75f_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!6VtD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cc37f6e-1bb3-4781-9de0-edf018fbb75f_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!6VtD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cc37f6e-1bb3-4781-9de0-edf018fbb75f_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6VtD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cc37f6e-1bb3-4781-9de0-edf018fbb75f_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0cc37f6e-1bb3-4781-9de0-edf018fbb75f_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1005763,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/213569114?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cc37f6e-1bb3-4781-9de0-edf018fbb75f_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6VtD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cc37f6e-1bb3-4781-9de0-edf018fbb75f_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!6VtD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cc37f6e-1bb3-4781-9de0-edf018fbb75f_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!6VtD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cc37f6e-1bb3-4781-9de0-edf018fbb75f_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!6VtD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cc37f6e-1bb3-4781-9de0-edf018fbb75f_1672x941.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2>What&#8217;s inside</h2><p><a href="https://open.substack.com/pub/cornellius/p/module-1-getting-the-question-right?r=3kmaf&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">Module 1</a> is available now. Modules 2 through 14 are coming soon, and I&#8217;ll link them here as they are published.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WFHF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf7b0d3-9891-4441-bf52-1b2c05640f32_1122x1402.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WFHF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf7b0d3-9891-4441-bf52-1b2c05640f32_1122x1402.png 424w, https://substackcdn.com/image/fetch/$s_!WFHF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf7b0d3-9891-4441-bf52-1b2c05640f32_1122x1402.png 848w, https://substackcdn.com/image/fetch/$s_!WFHF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf7b0d3-9891-4441-bf52-1b2c05640f32_1122x1402.png 1272w, https://substackcdn.com/image/fetch/$s_!WFHF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf7b0d3-9891-4441-bf52-1b2c05640f32_1122x1402.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WFHF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf7b0d3-9891-4441-bf52-1b2c05640f32_1122x1402.png" width="1122" height="1402" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6bf7b0d3-9891-4441-bf52-1b2c05640f32_1122x1402.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1402,&quot;width&quot;:1122,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1273074,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/213569114?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf7b0d3-9891-4441-bf52-1b2c05640f32_1122x1402.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!WFHF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf7b0d3-9891-4441-bf52-1b2c05640f32_1122x1402.png 424w, https://substackcdn.com/image/fetch/$s_!WFHF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf7b0d3-9891-4441-bf52-1b2c05640f32_1122x1402.png 848w, https://substackcdn.com/image/fetch/$s_!WFHF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf7b0d3-9891-4441-bf52-1b2c05640f32_1122x1402.png 1272w, https://substackcdn.com/image/fetch/$s_!WFHF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6bf7b0d3-9891-4441-bf52-1b2c05640f32_1122x1402.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><ol><li><p><strong><a href="https://open.substack.com/pub/cornellius/p/module-1-getting-the-question-right?r=3kmaf&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">Getting the question right before writing SQL</a></strong> <em>(free, available now).</em> Before writing the query, we decide what we actually want to measure.</p></li><li><p><strong>SELECT and FROM: reading the table</strong> <em>(coming soon).</em> We use <code>SELECT</code> and <code>FROM</code> to choose the data, but first we need to know what one row in the table represents.</p></li><li><p><strong>Filtering data with WHERE and NULL values</strong> <em>(coming soon).</em> A filter can remove more rows than we expect, especially when NULL values, dates, and time zones are involved.</p></li><li><p><strong>Finding the top results with ORDER BY and LIMIT</strong> <em>(coming soon).</em> We use sorting and limiting to find the top results, including how to handle ties.</p></li><li><p><strong>Counting the right thing with GROUP BY</strong> <em>(coming soon).</em> <code>COUNT</code> and <code>SUM</code> are easy to write, but we still need to make sure the grouped result matches what we want to count.</p></li><li><p><strong>WHERE and HAVING: filtering before and after GROUP BY</strong> <em>(coming soon).</em> We will see how the result changes depending on when the filter is applied.</p></li><li><p><strong>How to define a metric clearly</strong> <em>(coming soon).</em> We decide the numerator, denominator, and time period so someone else can calculate the same metric.</p></li><li><p><strong>Choosing between INNER JOIN and LEFT JOIN</strong> <em>(coming soon).</em> The join we choose determines which rows remain in the result and which disappear.</p></li><li><p><strong>When a join increases your numbers</strong> <em>(coming soon).</em> A one-to-many join can duplicate rows and increase the total count, so we will learn how to identify and verify it.</p></li><li><p><strong>Breaking a query into steps with subqueries and CTEs</strong> <em>(coming soon).</em> We use smaller query steps when the SQL becomes harder to read and check.</p></li><li><p><strong>Using window functions without changing the row count</strong> <em>(coming soon).</em> We will use partitions, ordering, and frames for calculations such as running totals and ranks.</p></li><li><p><strong>Checking whether your result is right</strong> <em>(coming soon).</em> A query that runs successfully is not enough; we compare the result against numbers we already know.</p></li><li><p><strong>When a SQL query needs optimization</strong> <em>(coming soon).</em> We review the execution plan and assess whether speeding up the query is actually necessary.</p></li><li><p><strong>Writing the result and recommendation</strong> <em>(coming soon).</em> We conclude by explaining the result, the assumptions we made, and our recommendations.</p><div><hr></div></li></ol><h2>What you&#8217;ll be able to do</h2><p>After finishing the course, you should be able to:</p><p>&#8226; turn a vague request into a clear question with a defined metric<br>&#8226; pull and filter data without accidentally dropping or duplicating rows<br>&#8226; define metrics that someone else can calculate and get the same result<br>&#8226; write joins without increasing your numbers by accident<br>&#8226; split a query into smaller, named steps<br>&#8226; check whether your result makes sense<br>&#8226; turn the result into a short report with a recommendation<br>&#8226; explain why you made a certain choice when someone asks</p><div><hr></div><h2>Who it&#8217;s for</h2><p><strong>If you&#8217;re looking for your first or next data role</strong>, this course can help you go beyond SQL syntax. In an interview, you might be asked why you chose a certain metric, which rows you counted, or how you know the result is correct. We will practice these kinds of questions throughout the course.</p><p><strong>If you&#8217;re still early in a data job</strong>, running the query is only one part of the work. You also need to check the result before sending it, explain anything that might affect the number, and know what to do when the result looks strange.</p><p><strong>If you review other people&#8217;s analysis</strong>, each module also has a manager&#8217;s view. This part shows what I would check and what questions I would ask when reviewing the work.</p><div><hr></div><h2>How each module works</h2><p>Every module is split into two parts.</p><h3>The SQL part</h3><p>&#8226; <strong>Concept:</strong> the SQL idea we are learning.<br>&#8226; <strong>How it works:</strong> what the query is doing and why we get that result.<br>&#8226; <strong>Worked example:</strong> we start with a query that gives the wrong result and then fix it in the playground.<br>&#8226; <strong>Predict the result:</strong> before seeing the answer, you try to guess what the query will return.<br>&#8226; <strong>Common mistakes:</strong> things that can easily go wrong with the query.</p><h3>The decision part</h3><p>&#8226; <strong>Your decision:</strong> what you need to choose before using the result.<br>&#8226; <strong>When it changes:</strong> cases where the usual approach might not work.<br>&#8226; <strong>How to explain it:</strong> what you can say about the result and the choice you made.<br>&#8226; <strong>In an interview:</strong> questions you might get about the topic.<br>&#8226; <strong>Manager&#8217;s view:</strong> what I would check when reviewing someone else&#8217;s analysis.</p><p>There are also two exercises in every module. The first one uses the course data, and the second asks you to apply the same idea to your own data. You&#8217;ll also get a template you can reuse.</p><p>The course gives you less help as you go. In the early modules, I show you the whole example. In the middle modules, you need to complete part of the query yourself. In the last few modules, I give you the problem first and let you try it before showing the answer.</p><div><hr></div><h2>What you should know already</h2><p>If you can write a <code>SELECT</code> with a <code>WHERE</code> clause and have tried a join before, you&#8217;re good to start. It&#8217;s fine if joins still feel confusing.</p><p>You don&#8217;t need to know CTEs, window functions, or query tuning yet. We will learn those in the later modules.</p><p>If SQL is completely new to you, I would suggest doing any free introduction to <code>SELECT</code> and <code>WHERE</code> first. Then you can come back here. This course starts after the basic syntax because I want to spend more time on deciding what to count, checking the result, and explaining your choices.</p><div><hr></div><h2>The dataset and what you build</h2><p>Every module has a live SQL playground. Click the link; the tables are already there, and you can run the query while you read. You don&#8217;t need an account, and you don&#8217;t need to install anything.</p><p>The <a href="https://github.com/CornelliusYW/NBD-SQL-and-Analytic-Course">course SQL scripts are also available on GitHub</a> if you want to keep them.</p><p>The examples use small samples from real e-commerce data. I keep them small so you can read the rows and see exactly where a wrong number comes from.</p><p>At the end of the course, you will choose your own question and do the analysis using the same process. This becomes your course project, which you can use in an application or talk about during an interview.</p><div><hr></div><h2>What you earn</h2><p>The course has a <strong>Certificate of Completion</strong>, and I also plan to add a separate <strong>Verified Project</strong> later.</p><p>To get the Certificate of Completion, you need to finish all fourteen modules and their &#8220;Try it&#8221; activities. There is no timed exam.</p><p>At the end, you fill in a short form with your name and email, confirm that you finished the work, and write a short reflection about one decision that changed your analysis. Once I check it, you&#8217;ll receive a digital certificate with your name and its own verification page.</p><p>The <strong>Verified Project</strong> will be different. I plan to add it once the rubric and review process are ready. You&#8217;ll do an analysis using your own question, explain some of the decisions you made, and answer questions about your work. The certificate will also link to the reviewed analysis.</p><p>So, the Certificate of Completion shows that you finished the course. The Verified Project will show that your project was also reviewed.</p><p>Neither one is an accredited SQL certification.</p><p><a href="https://credsverse.com/credentials/dd81fd0e-c9c0-432f-84f5-4e8e0dc94522?preview=1">See a sample Certificate of Completion</a>.</p><div><hr></div><h2>Why I made this</h2><p>I&#8217;ve worked with and reviewed a lot of SQL over the years, and I keep seeing similar problems.</p><p>For example, a join can double-count rows, two people can calculate the same metric differently, or a query can answer a different question than the one we originally wanted. The SQL itself can still be correct.</p><p>In my experience, writing the query is often not the hardest part. The harder part is knowing what to count, what to check, and whether the result actually makes sense.</p><p>Most SQL tutorials naturally spend a lot of time teaching the syntax. I want this course to spend more time on what happens after that. We will still learn SQL, but we will also learn how to check the result and explain why we made certain choices.</p><div><hr></div><h2>Common questions</h2><p><strong>Do I need to know SQL already?</strong></p><p>Some. If you can use <code>SELECT</code> and <code>WHERE</code> and have tried a join before, that is enough to start. If you have never written SQL before, I would suggest taking a free beginner SQL tutorial first and then coming back.</p><p><strong>Will this help me get a job?</strong></p><p>I hope it helps you prepare, but no course can promise you a job. What we can do is practice the SQL and analysis questions you might get in an interview. You will also finish with an analysis that you can show or talk about when applying for a role.</p><p><strong>Is the certificate a professional SQL certification?</strong></p><p>No. It is a Certificate of Completion for finishing the fourteen modules and their exercises. The Verified Project I plan to add later will include a project review, but that will not be an accredited certification either.</p><p><strong>I already work as an analyst. Is it too basic?</strong></p><p>The first few modules might be easy if you already use SQL every day. The later modules go more into metric definitions, joins that change your numbers, checking results, query performance, and reporting.</p><p><strong>What do I need to follow along?</strong></p><p>Only a browser. The data is already loaded in the SQL playground, so you don&#8217;t need to install anything or create an account.</p><p><strong>Does the SQL work anywhere else?</strong></p><p>The playground uses PostgreSQL, so the examples use PostgreSQL syntax. Most of the SQL will look familiar elsewhere, but if something works differently in MySQL or BigQuery, I&#8217;ll mention it in the module.</p><p><strong>Is any of it free?</strong></p><p>Yes. Module 1 is completely free. The other modules are for paid subscribers.</p><div><hr></div><h2>Start here</h2><p>Module 1, <strong><a href="https://open.substack.com/pub/cornellius/p/module-1-getting-the-question-right?r=3kmaf&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">Getting the question right before any SQL</a></strong>, is free and takes around fifteen minutes.</p><p>We start with a simple request for one number. The query gives us an answer, but when we look more closely at the question, we see there are still a few things we need to decide before using that number.</p><p>You can run all the SQL in the playground as you read, then try the exercise yourself.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://open.substack.com/pub/cornellius/p/module-1-getting-the-question-right?r=3kmaf&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true&quot;,&quot;text&quot;:&quot;Module 1&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://open.substack.com/pub/cornellius/p/module-1-getting-the-question-right?r=3kmaf&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true"><span>Module 1</span></a></p><p>I&#8217;ll add the other modules here as they are published. You can subscribe to receive a release notice when a new one is ready.</p>]]></content:encoded></item><item><title><![CDATA[3 Things I Learned Testing One Market Data API Across 9 Markets]]></title><description><![CDATA[Building a live price alert dashboard across stocks, forex, futures, and crypto]]></description><link>https://www.nb-data.com/p/3-things-i-learned-testing-one-market</link><guid isPermaLink="false">https://www.nb-data.com/p/3-things-i-learned-testing-one-market</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Thu, 13 Aug 2026 01:57:24 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxmaW5hbmNlfGVufDB8fHx8MTc4NjQ1Njc4MHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxmaW5hbmNlfGVufDB8fHx8MTc4NjQ1Njc4MHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxmaW5hbmNlfGVufDB8fHx8MTc4NjQ1Njc4MHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxmaW5hbmNlfGVufDB8fHx8MTc4NjQ1Njc4MHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxmaW5hbmNlfGVufDB8fHx8MTc4NjQ1Njc4MHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxmaW5hbmNlfGVufDB8fHx8MTc4NjQ1Njc4MHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxmaW5hbmNlfGVufDB8fHx8MTc4NjQ1Njc4MHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="6000" height="4000" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxmaW5hbmNlfGVufDB8fHx8MTc4NjQ1Njc4MHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:4000,&quot;width&quot;:6000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;screen showing bitcoin trading chart&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="screen showing bitcoin trading chart" title="screen showing bitcoin trading chart" srcset="https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxmaW5hbmNlfGVufDB8fHx8MTc4NjQ1Njc4MHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxmaW5hbmNlfGVufDB8fHx8MTc4NjQ1Njc4MHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxmaW5hbmNlfGVufDB8fHx8MTc4NjQ1Njc4MHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxmaW5hbmNlfGVufDB8fHx8MTc4NjQ1Njc4MHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@nick604">Nick Chong</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>Most market data providers don&#8217;t cover everything. So we end up with one vendor for US equities, another for Asia, and another for forex or crypto. Each has its own authentication, endpoints, and field names.</p><p><span>I chose&nbsp;</span><a href="https://infoway.io/en"><span>Infoway</span></a><span>&nbsp;because it covers six equity markets alongside forex, futures, and crypto, with real-time streaming across all of them. I wanted to see how well that promise held up in practice, so I built a price-alert dashboard that monitors nine markets simultaneously.</span></p><p>The build starts with live prices and historical candles, then moves to order-book data and streaming, and finally brings everything together into a single dashboard with alerts against a previous-close baseline.</p><p>Let&#8217;s get into it.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2><span>What the API covers</span></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!K8NT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59913661-9287-4646-804b-57db7d94e04d_1247x672.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!K8NT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59913661-9287-4646-804b-57db7d94e04d_1247x672.png 424w, https://substackcdn.com/image/fetch/$s_!K8NT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59913661-9287-4646-804b-57db7d94e04d_1247x672.png 848w, https://substackcdn.com/image/fetch/$s_!K8NT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59913661-9287-4646-804b-57db7d94e04d_1247x672.png 1272w, https://substackcdn.com/image/fetch/$s_!K8NT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59913661-9287-4646-804b-57db7d94e04d_1247x672.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!K8NT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59913661-9287-4646-804b-57db7d94e04d_1247x672.png" width="1247" height="672" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/59913661-9287-4646-804b-57db7d94e04d_1247x672.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:672,&quot;width&quot;:1247,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!K8NT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59913661-9287-4646-804b-57db7d94e04d_1247x672.png 424w, https://substackcdn.com/image/fetch/$s_!K8NT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59913661-9287-4646-804b-57db7d94e04d_1247x672.png 848w, https://substackcdn.com/image/fetch/$s_!K8NT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59913661-9287-4646-804b-57db7d94e04d_1247x672.png 1272w, https://substackcdn.com/image/fetch/$s_!K8NT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59913661-9287-4646-804b-57db7d94e04d_1247x672.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Figure 1. The Infoway API landing page</em></figcaption></figure></div><p>A market data API does two things. It tells us what an asset is trading at now or where it closed last Tuesday, and it streams new prices as trades occur. <a href="https://infoway.io/en">Infoway</a> supports both across <strong>every market in the table below</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CAHB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52c77e3a-3f1b-438d-b91d-1d5faf29299e_1281x578.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CAHB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52c77e3a-3f1b-438d-b91d-1d5faf29299e_1281x578.png 424w, https://substackcdn.com/image/fetch/$s_!CAHB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52c77e3a-3f1b-438d-b91d-1d5faf29299e_1281x578.png 848w, https://substackcdn.com/image/fetch/$s_!CAHB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52c77e3a-3f1b-438d-b91d-1d5faf29299e_1281x578.png 1272w, https://substackcdn.com/image/fetch/$s_!CAHB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52c77e3a-3f1b-438d-b91d-1d5faf29299e_1281x578.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CAHB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52c77e3a-3f1b-438d-b91d-1d5faf29299e_1281x578.png" width="1281" height="578" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/52c77e3a-3f1b-438d-b91d-1d5faf29299e_1281x578.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:578,&quot;width&quot;:1281,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:59305,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/210756960?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52c77e3a-3f1b-438d-b91d-1d5faf29299e_1281x578.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!CAHB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52c77e3a-3f1b-438d-b91d-1d5faf29299e_1281x578.png 424w, https://substackcdn.com/image/fetch/$s_!CAHB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52c77e3a-3f1b-438d-b91d-1d5faf29299e_1281x578.png 848w, https://substackcdn.com/image/fetch/$s_!CAHB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52c77e3a-3f1b-438d-b91d-1d5faf29299e_1281x578.png 1272w, https://substackcdn.com/image/fetch/$s_!CAHB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F52c77e3a-3f1b-438d-b91d-1d5faf29299e_1281x578.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Table 1: Infoway features card</figcaption></figure></div><div><hr></div><h2><span>Pull live prices from nine markets</span></h2><p>The first test is simple: can the same client retrieve the latest traded price across every market?</p><p>Here is the REST client used throughout the article:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import requests
 
BASE_URL = "https://data.infoway.io"
HEADERS = {"apiKey": API_KEY, "Accept": "application/json"}
 
def _unwrap(resp):
    resp.raise_for_status()
    payload = resp.json()
    if payload.get("ret") != 200:
        raise RuntimeError(f"Infoway API error {payload.get('ret')}: {payload.get('msg')}")
    return payload["data"]
 
def infoway_get(path: str, params: dict | None = None) -&gt; dict:
    return _unwrap(requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params, timeout=10))
 
def infoway_post(path: str, body: dict) -&gt; dict:
    return _unwrap(requests.post(f"{BASE_URL}{path}", headers=HEADERS, json=body, timeout=10))
</code></pre></div><p>Authentication uses a single <code>apiKey</code> header. Responses also follow the same envelope: <code>ret</code> for status, <code>msg</code> for the message, <code>traceId</code> for tracing, and <code>data</code> for the result. The helper above checks the first two and returns <code>data</code>.</p><p>The question is whether that same client works across all nine markets.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">MARKET_SAMPLES = {
    "US Stocks":            ("stock",  "TSLA.US,AAPL.US"),
    "China A-Shares":       ("stock",  "600519.SH,000001.SZ"),
    "Hong Kong Stocks":     ("stock",  "00700.HK,09988.HK"),
    "Japan Stocks":         ("japan",  "7203.JP,6758.JP"),
    "South Korea Stocks":   ("korea",  "005930.KS,000660.KS"),
    "India Stocks":         ("india",  "RELIANCE.IN,INFY.IN"),
    "Forex":                ("common", "EURUSD,USDJPY"),
    "Futures/Commodities":  ("common", "XAUUSD,CL.FUT"),
    "Crypto":               ("crypto", "BTCUSDT,ETHUSDT"),
}
 
rows = []
for label, (business, codes) in MARKET_SAMPLES.items():
    for item in infoway_get(f"/{business}/batch_trade/{codes}"):
        rows.append({"market": label, "symbol": item["s"], "price": item["p"],
                     "volume": item["v"], "timestamp": item["t"]})
 
df_trades = pd.DataFrame(rows)
</code></pre></div><p>Only the endpoint category and symbols change between markets. The returned fields remain consistent: <code>s</code> for symbol, <code>p</code> for price, <code>v</code> for volume, and <code>t</code> for the timestamp in milliseconds.</p><p>All nine markets returned data successfully.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tx8O!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ce30df-f513-4552-98d9-c37cdcdbfbe7_1247x376.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tx8O!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ce30df-f513-4552-98d9-c37cdcdbfbe7_1247x376.png 424w, https://substackcdn.com/image/fetch/$s_!tx8O!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ce30df-f513-4552-98d9-c37cdcdbfbe7_1247x376.png 848w, https://substackcdn.com/image/fetch/$s_!tx8O!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ce30df-f513-4552-98d9-c37cdcdbfbe7_1247x376.png 1272w, https://substackcdn.com/image/fetch/$s_!tx8O!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ce30df-f513-4552-98d9-c37cdcdbfbe7_1247x376.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tx8O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ce30df-f513-4552-98d9-c37cdcdbfbe7_1247x376.png" width="1247" height="376" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f9ce30df-f513-4552-98d9-c37cdcdbfbe7_1247x376.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:376,&quot;width&quot;:1247,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tx8O!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ce30df-f513-4552-98d9-c37cdcdbfbe7_1247x376.png 424w, https://substackcdn.com/image/fetch/$s_!tx8O!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ce30df-f513-4552-98d9-c37cdcdbfbe7_1247x376.png 848w, https://substackcdn.com/image/fetch/$s_!tx8O!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ce30df-f513-4552-98d9-c37cdcdbfbe7_1247x376.png 1272w, https://substackcdn.com/image/fetch/$s_!tx8O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9ce30df-f513-4552-98d9-c37cdcdbfbe7_1247x376.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Table 2. Live prices from nine markets</em></figcaption></figure></div><p>Equity symbols use exchange suffixes such as <code>.SH</code> and <code>.SZ</code> for China, <code>.HK</code> for Hong Kong, <code>.JP</code> for Japan, and <code>.KS</code> for South Korea.</p><p>The last traded price tells us what just happened. The order book shows what is waiting to happen next.</p><div><hr></div><h2><span>Read the order book</span></h2><p>The order book shows the prices and volumes currently waiting to trade. The highest bid is the best price available to a seller, the lowest ask is the best price available to a buyer, and the gap between them is the spread.</p><p>For BTCUSDT, the endpoint is a single call:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">depth = infoway_get("/crypto/batch_depth/BTCUSDT")</code></pre></div><p>Trimmed to three levels, the response looks like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">[{'s': 'BTCUSDT', 't': 1786454834539,
  'a': [['64181.72', '64181.73', '64181.74'],
        ['6.44259',  '0.00066',  '0.00016']],
  'b': [['64181.71', '64181.70', '64181.69'],
        ['11.42786', '0.00073', '0.00016']]}]
</code></pre></div><p><code>a</code> contains the ask side and <code>b</code> the bid side. Each holds prices first, followed by the volume available at those prices. The full response contains ten levels per side.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6KGY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8bde93-8827-4cf4-9e51-72e44b408211_1248x332.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6KGY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8bde93-8827-4cf4-9e51-72e44b408211_1248x332.png 424w, https://substackcdn.com/image/fetch/$s_!6KGY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8bde93-8827-4cf4-9e51-72e44b408211_1248x332.png 848w, https://substackcdn.com/image/fetch/$s_!6KGY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8bde93-8827-4cf4-9e51-72e44b408211_1248x332.png 1272w, https://substackcdn.com/image/fetch/$s_!6KGY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8bde93-8827-4cf4-9e51-72e44b408211_1248x332.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6KGY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8bde93-8827-4cf4-9e51-72e44b408211_1248x332.png" width="1248" height="332" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ad8bde93-8827-4cf4-9e51-72e44b408211_1248x332.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:332,&quot;width&quot;:1248,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6KGY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8bde93-8827-4cf4-9e51-72e44b408211_1248x332.png 424w, https://substackcdn.com/image/fetch/$s_!6KGY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8bde93-8827-4cf4-9e51-72e44b408211_1248x332.png 848w, https://substackcdn.com/image/fetch/$s_!6KGY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8bde93-8827-4cf4-9e51-72e44b408211_1248x332.png 1272w, https://substackcdn.com/image/fetch/$s_!6KGY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad8bde93-8827-4cf4-9e51-72e44b408211_1248x332.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Table 3. BTCUSDT order book, top five levels</em></figcaption></figure></div><p>The spread here is only one cent, but the depth reveals what the spread cannot: most of the available volume sits at the top level on both sides.</p><p>The dashboard itself only needs the latest price and a historical baseline, so the next step is to retrieve the candles.</p><div><hr></div><h2><span>Pull the historical candles</span></h2><p>The alert needs a baseline, so I use the close from the latest completed daily candle. I also pulled 90 daily candles for TSLA and AAPL to check the historical endpoint.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">frames = {}
for symbol in ["TSLA.US", "AAPL.US"]:
    result = infoway_post("/stock/v2/batch_kline", {"klineType": 8, "klineNum": 90, "codes": symbol})
    frame = pd.DataFrame(result[0]["respList"])
    frame["t"] = pd.to_datetime(frame["t"].astype(int), unit="s")
    for col in ["o", "h", "l", "c", "v"]:
        frame[col] = frame[col].astype(float)
    frames[symbol] = frame.sort_values("t")
</code></pre></div><p>My first attempt sent both symbols in a single request. It returned only two candles per symbol, with no error and no indication that the response was incomplete. The limit is documented; I had simply missed it. Requesting one symbol at a time returned the full 90 candles.</p><p><strong>First thing I learned is that a silent truncation costs more than errors.</strong> Errors make the problem visible; partial responses can look correct and send debugging in the wrong direction.</p><p>The response also requires two small conversions. Timestamps arrive in seconds here, while the trade endpoint uses milliseconds, and prices arrive as text rather than as numbers.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!95db!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffacc70db-5d00-4bc9-84f8-f612b876cb3a_1959x776.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!95db!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffacc70db-5d00-4bc9-84f8-f612b876cb3a_1959x776.png 424w, https://substackcdn.com/image/fetch/$s_!95db!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffacc70db-5d00-4bc9-84f8-f612b876cb3a_1959x776.png 848w, https://substackcdn.com/image/fetch/$s_!95db!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffacc70db-5d00-4bc9-84f8-f612b876cb3a_1959x776.png 1272w, https://substackcdn.com/image/fetch/$s_!95db!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffacc70db-5d00-4bc9-84f8-f612b876cb3a_1959x776.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!95db!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffacc70db-5d00-4bc9-84f8-f612b876cb3a_1959x776.png" width="1456" height="577" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/facc70db-5d00-4bc9-84f8-f612b876cb3a_1959x776.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:577,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:250812,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/210756960?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffacc70db-5d00-4bc9-84f8-f612b876cb3a_1959x776.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!95db!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffacc70db-5d00-4bc9-84f8-f612b876cb3a_1959x776.png 424w, https://substackcdn.com/image/fetch/$s_!95db!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffacc70db-5d00-4bc9-84f8-f612b876cb3a_1959x776.png 848w, https://substackcdn.com/image/fetch/$s_!95db!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffacc70db-5d00-4bc9-84f8-f612b876cb3a_1959x776.png 1272w, https://substackcdn.com/image/fetch/$s_!95db!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffacc70db-5d00-4bc9-84f8-f612b876cb3a_1959x776.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure 2. Daily close, last 90 trading sessions</figcaption></figure></div><p>At 600 requests a minute, looping a few hundred symbols one at a time is nothing.</p><p>That&#8217;s the past covered. Live prices arrive via a different interface.</p><div><hr></div><h2><span>Stream one channel</span></h2><p>The historical candles give us the baseline. Now the dashboard needs prices as they move.</p><p>Polling would mean repeatedly calling the REST API for each symbol. Across nine markets, checking each one every second would quickly hit the rate limit. Streaming is a better fit: subscribe once, then receive trades as they happen.</p><p>Infoway uses the same WebSocket URL across markets, with the <code>business</code> parameter distinguishing stock, crypto, common, Japan, India, and Korea. For the first test, I subscribed only to BTCUSDT.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">async def read_json(ws, timeout):
    """Read one WS message; returns None on timeout or a malformed frame."""
    try:
        return json.loads(await asyncio.wait_for(ws.recv(), timeout=timeout))
    except (asyncio.TimeoutError, json.JSONDecodeError):
        return None
 
async def stream_demo(max_messages: int = 15, timeout_sec: int = 30):
    received = []
    async with websockets.connect(WS_URL) as ws:
        await ws.send(json.dumps({"code": 10000, "trace": str(uuid.uuid4()), "data": {"codes": "BTCUSDT"}}))
        await ws.send(json.dumps({"code": 10006, "trace": str(uuid.uuid4()),
                                  "data": {"arr": [{"type": 1, "codes": "BTCUSDT"}]}}))
        start = time.time()
        while len(received) &lt; max_messages and (time.time() - start) &lt; timeout_sec:
            msg = await read_json(ws, timeout=5)
            if not msg:
                continue
            received.append(msg)
            print(f"[{LABELS.get(msg.get('code'), msg.get('code'))}] {msg.get('data')}")
    return received</code></pre></div><p>Trades and candles use separate subscriptions on the same connection, which is why there are two <code>send</code> calls. <code>read_json</code> keeps the stream running through timeouts or malformed frames instead of stopping the loop.</p><p>The connection acknowledged both subscriptions and started returning trades within a second:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">[CONNECTED] None
[SUB_ACK(trade)] None
[SUB_ACK(kline)] None
[TRADE] {'p': '65102.26', 's': 'BTCUSDT', 't': 1786282257136, 'td': 2, 'v': '0.00173'}
[TRADE] {'p': '65102.26', 's': 'BTCUSDT', 't': 1786282257136, 'td': 2, 'v': '0.00173'}
[TRADE] {'p': '65102.27', 's': 'BTCUSDT', 't': 1786282257471, 'td': 1, 'v': '0.0028'}
[TRADE] {'p': '65102.27', 's': 'BTCUSDT', 't': 1786282257471, 'td': 1, 'v': '0.0028'}</code></pre></div><p>One channel works. Next, I test whether the same pattern holds across the six channel types covering all nine markets.</p><div><hr></div><h2><span>Build the dashboard</span></h2><p>With the historical baseline and one streaming channel working, the final step is to put them together: load the baseline, verify each channel, then stream them concurrently and fire alerts.</p><h3>Pull the baseline</h3><p><span>Each alert compares the live price with the previous completed daily close. For every symbol, I fetch the last two daily candles and keep the earlier close as the reference point.</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">BASELINE_SYMBOLS = {
    "stock":  [("TSLA.US", "US Stocks"), ("00700.HK", "Hong Kong"), ("600519.SH", "China A-Shares")],
    "japan":  [("6758.JP", "Japan"), ("7203.JP", "Japan")],
    "korea":  [("005930.KS", "South Korea"), ("000660.KS", "South Korea")],
    "india":  [("RELIANCE.IN", "India"), ("INFY.IN", "India")],
    "common": [("EURUSD", "Forex"), ("XAUUSD", "Futures/Commodities")],
    "crypto": [("BTCUSDT", "Crypto"), ("ETHUSDT", "Crypto"), ("SOLUSDT", "Crypto")],
}
 
baseline = {}
for business, entries in BASELINE_SYMBOLS.items():
    for symbol, market in entries:
        result = infoway_post(f"/{business}/v2/batch_kline",
                              {"klineType": 8, "klineNum": 2, "codes": symbol})
        baseline[symbol] = {"market": market, "business": business,
                            "prev_close": float(result[0]["respList"][0]["c"])}
</code></pre></div><p>That gives the streaming code a dictionary for symbol lookup.</p><h3>Verify every channel</h3><p>Before opening everything at once, I tested one symbol from each of the six channel types.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">async def prove_channel(business: str, symbol: str, timeout_sec: int = 8) -&gt; dict:
    url = f"wss://data.infoway.io/ws?business={business}&amp;apikey={API_KEY}"
    start = time.time()
    subscribed_in, live_price = None, None
    try:
        async with websockets.connect(url) as ws:
            await ws.send(json.dumps({"code": 10000, "trace": str(uuid.uuid4()),
                                      "data": {"codes": symbol}}))
            while time.time() - start &lt; timeout_sec:
                msg = await read_json(ws, timeout=timeout_sec - (time.time() - start))
                if not msg:
                    continue
                if msg.get("code") == 10001 and subscribed_in is None:
                    subscribed_in = round(time.time() - start, 2)
                elif msg.get("code") == 10002:
                    live_price = msg["data"]["p"]
                    break
    except Exception:
        pass
    status = "SUBSCRIBED + LIVE TICK" if live_price else (
             "SUBSCRIBED" if subscribed_in is not None else "FAILED")
    return {"business": business, "symbol": symbol, "status": status,
            "subscribed_in_s": subscribed_in, "live_price": live_price}
 
PROOF_TARGETS = [
    ("stock", "TSLA.US"), ("crypto", "BTCUSDT"), ("common", "EURUSD"),
    ("japan", "6758.JP"), ("india", "RELIANCE.IN"), ("korea", "005930.KS"),
</code></pre></div><p>The test separates two signals: a subscription acknowledgement means the channel accepted the request, while a live tick confirms that trades are actually arriving.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!t4Sg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662f868a-f817-48a9-b606-35c5ad9fabd5_1247x400.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!t4Sg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662f868a-f817-48a9-b606-35c5ad9fabd5_1247x400.png 424w, https://substackcdn.com/image/fetch/$s_!t4Sg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662f868a-f817-48a9-b606-35c5ad9fabd5_1247x400.png 848w, https://substackcdn.com/image/fetch/$s_!t4Sg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662f868a-f817-48a9-b606-35c5ad9fabd5_1247x400.png 1272w, https://substackcdn.com/image/fetch/$s_!t4Sg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662f868a-f817-48a9-b606-35c5ad9fabd5_1247x400.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!t4Sg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662f868a-f817-48a9-b606-35c5ad9fabd5_1247x400.png" width="1247" height="400" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/662f868a-f817-48a9-b606-35c5ad9fabd5_1247x400.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:400,&quot;width&quot;:1247,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!t4Sg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662f868a-f817-48a9-b606-35c5ad9fabd5_1247x400.png 424w, https://substackcdn.com/image/fetch/$s_!t4Sg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662f868a-f817-48a9-b606-35c5ad9fabd5_1247x400.png 848w, https://substackcdn.com/image/fetch/$s_!t4Sg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662f868a-f817-48a9-b606-35c5ad9fabd5_1247x400.png 1272w, https://substackcdn.com/image/fetch/$s_!t4Sg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F662f868a-f817-48a9-b606-35c5ad9fabd5_1247x400.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Table 4. Comparison of the market channel status</em></figcaption></figure></div><p>All six channels acknowledged the subscription within half a second. The three markets trading at the time also returned live prices. Japan, Korea, and India were not producing trades, but their channels acknowledged just as quickly as the others.</p><p><strong>The second thing I learned: a channel can be verified without waiting for a trade.</strong> A subscription acknowledgement already confirms that the connection works and the request was accepted, even when the market is closed and no prices are moving.</p><h3>Stream all six channels at once</h3><p>The previous test opened each connection separately. The dashboard needs all six running at the same time.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">CHANNELS = {
    "stock":  {"symbols": "TSLA.US,00700.HK",        "label": "Stocks (US + HK)"},
    "crypto": {"symbols": "BTCUSDT,ETHUSDT,SOLUSDT", "label": "Crypto"},
    "common": {"symbols": "EURUSD,XAUUSD",           "label": "Forex + Futures/Commodities"},
    "japan":  {"symbols": "6758.JP,7203.JP",         "label": "Japan"},
    "korea":  {"symbols": "005930.KS,000660.KS",     "label": "South Korea"},
    "india":  {"symbols": "RELIANCE.IN,INFY.IN",     "label": "India"},
}
ALERT_THRESHOLD_PCT = 0.05
 
async def stream_channel(business: str, symbols: str, label: str, duration: int = 45):
    url = f"wss://data.infoway.io/ws?business={business}&amp;apikey={API_KEY}"
    async with websockets.connect(url) as ws:
        await ws.send(json.dumps({"code": 10000, "trace": str(uuid.uuid4()),
                                  "data": {"codes": symbols}}))
        start = time.time()
        while time.time() - start &lt; duration:
            msg = await read_json(ws, timeout=5)
            if not msg or msg.get("code") != 10002:
                continue
            d = msg["data"]
            sym, price = d["s"], float(d["p"])
            tick = {"market": label, "symbol": sym, "price": price,
                    "ts": d["t"], "recv_s": time.time()}
            base = baseline.get(sym)
            if base:
                tick["move_pct"] = round((price - base["prev_close"]) / base["prev_close"] * 100, 4)
                is_alert = abs(tick["move_pct"]) &gt;= ALERT_THRESHOLD_PCT
                if is_alert and not alert_state.get(sym, False):
                    alerts.append(tick)
                alert_state[sym] = is_alert
            tape.append(tick)
 
async def run_dashboard(duration: int = 45):
    names = list(CHANNELS)
    results = await asyncio.gather(*[
        stream_channel(biz, CHANNELS[biz]["symbols"], CHANNELS[biz]["label"], duration)
        for biz in names
    ], return_exceptions=True)
    for biz, r in zip(names, results):
        if isinstance(r, Exception):
            print(f"  channel {biz} dropped: {type(r).__name__}: {r}")
</code></pre></div><p><code>asyncio.gather()</code> keeps the six connections alive concurrently, while each incoming trade is compared with its stored baseline. In my 45-second test, the open markets produced 1,608 ticks across six symbols.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!M4a_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd88ee135-78c1-4294-88b4-00180d513a25_1247x562.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!M4a_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd88ee135-78c1-4294-88b4-00180d513a25_1247x562.png 424w, https://substackcdn.com/image/fetch/$s_!M4a_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd88ee135-78c1-4294-88b4-00180d513a25_1247x562.png 848w, https://substackcdn.com/image/fetch/$s_!M4a_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd88ee135-78c1-4294-88b4-00180d513a25_1247x562.png 1272w, https://substackcdn.com/image/fetch/$s_!M4a_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd88ee135-78c1-4294-88b4-00180d513a25_1247x562.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!M4a_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd88ee135-78c1-4294-88b4-00180d513a25_1247x562.png" width="1247" height="562" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d88ee135-78c1-4294-88b4-00180d513a25_1247x562.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:562,&quot;width&quot;:1247,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!M4a_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd88ee135-78c1-4294-88b4-00180d513a25_1247x562.png 424w, https://substackcdn.com/image/fetch/$s_!M4a_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd88ee135-78c1-4294-88b4-00180d513a25_1247x562.png 848w, https://substackcdn.com/image/fetch/$s_!M4a_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd88ee135-78c1-4294-88b4-00180d513a25_1247x562.png 1272w, https://substackcdn.com/image/fetch/$s_!M4a_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd88ee135-78c1-4294-88b4-00180d513a25_1247x562.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Figure 3. Stream 6 channel at once for 45 seconds chart</em></figcaption></figure></div><p><code>alert_state</code> prevents the same movement from generating an alert on every subsequent trade. An alert fires when a symbol crosses the threshold, then waits until it falls back below before it can trigger again.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ic4_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa770ef51-9da1-4df8-a17e-d881f6b3257e_1180x500.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ic4_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa770ef51-9da1-4df8-a17e-d881f6b3257e_1180x500.png 424w, https://substackcdn.com/image/fetch/$s_!Ic4_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa770ef51-9da1-4df8-a17e-d881f6b3257e_1180x500.png 848w, https://substackcdn.com/image/fetch/$s_!Ic4_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa770ef51-9da1-4df8-a17e-d881f6b3257e_1180x500.png 1272w, https://substackcdn.com/image/fetch/$s_!Ic4_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa770ef51-9da1-4df8-a17e-d881f6b3257e_1180x500.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ic4_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa770ef51-9da1-4df8-a17e-d881f6b3257e_1180x500.png" width="1180" height="500" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a770ef51-9da1-4df8-a17e-d881f6b3257e_1180x500.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:500,&quot;width&quot;:1180,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Ic4_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa770ef51-9da1-4df8-a17e-d881f6b3257e_1180x500.png 424w, https://substackcdn.com/image/fetch/$s_!Ic4_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa770ef51-9da1-4df8-a17e-d881f6b3257e_1180x500.png 848w, https://substackcdn.com/image/fetch/$s_!Ic4_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa770ef51-9da1-4df8-a17e-d881f6b3257e_1180x500.png 1272w, https://substackcdn.com/image/fetch/$s_!Ic4_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa770ef51-9da1-4df8-a17e-d881f6b3257e_1180x500.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Table 5. Move alerts fired</em></figcaption></figure></div><p style="text-align: center;"></p><p>SOLUSDT crossed that boundary three times because its movement sat almost exactly on the threshold.</p><p>There was one more problem in the stream: a few trade messages carried timestamps years into the future. The prices in those messages looked normal, but a single bad timestamp was enough to distort the entire time axis.</p><p><strong>The third thing I learned: don&#8217;t rely on a remote timestamp for the chart.</strong> I record when each tick arrives locally and use that receipt time instead.</p><div><hr></div><h2>Conclusion</h2><p>Would I use it? Yes. The main value is having <strong>one integration instead of several</strong>, with the same REST client and streaming pattern working across every market I tested.</p><p>I am not a market data specialist, so I would treat this as a practical integration test rather than a verdict on market-data quality. The three lessons are what I would carry into the next provider evaluation:</p><ol><li><p><strong>Silent truncation costs more than errors.</strong> Errors make the problem visible; partial responses can look correct and send debugging in the wrong direction.</p></li><li><p><strong>A streaming channel can be verified without waiting for a trade.</strong> A subscription acknowledgement already tells us that the connection works and the request was accepted, even when the market is closed.</p></li><li><p><strong>Don&#8217;t rely on remote timestamps for the chart.</strong> Record when each tick arrives locally and use that receipt time instead.</p></li></ol><p>Those checks are small, but they reveal much more than a successful API call alone.</p>]]></content:encoded></item><item><title><![CDATA[Stock API: What it is and Why it Matters]]></title><description><![CDATA[How I decide on a data provider before writing the integration, and why that choice outlasts the code.]]></description><link>https://www.nb-data.com/p/stock-api-what-it-is-and-why-it-matter</link><guid isPermaLink="false">https://www.nb-data.com/p/stock-api-what-it-is-and-why-it-matter</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Sun, 26 Jul 2026 16:32:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!vn6D!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5606de-45db-4b2f-8ae7-80878b68b59f_1400x933.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vn6D!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5606de-45db-4b2f-8ae7-80878b68b59f_1400x933.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vn6D!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5606de-45db-4b2f-8ae7-80878b68b59f_1400x933.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vn6D!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5606de-45db-4b2f-8ae7-80878b68b59f_1400x933.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vn6D!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5606de-45db-4b2f-8ae7-80878b68b59f_1400x933.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vn6D!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5606de-45db-4b2f-8ae7-80878b68b59f_1400x933.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vn6D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5606de-45db-4b2f-8ae7-80878b68b59f_1400x933.jpeg" width="1400" height="933" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4f5606de-45db-4b2f-8ae7-80878b68b59f_1400x933.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:933,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!vn6D!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5606de-45db-4b2f-8ae7-80878b68b59f_1400x933.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vn6D!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5606de-45db-4b2f-8ae7-80878b68b59f_1400x933.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vn6D!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5606de-45db-4b2f-8ae7-80878b68b59f_1400x933.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vn6D!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f5606de-45db-4b2f-8ae7-80878b68b59f_1400x933.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@toti1996?utm_source=medium&amp;utm_medium=referral">T&#246;t&#246;s &#193;d&#225;m</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure></div><p>The hard part of using a stock API is not getting the data. It is choosing the provider, and you live with that choice long after the integration is done.</p><p>This is why I want to walk through what a stock API is, the two routes to its data, and how I decide whether a provider is worth building on. This article is for two types of readers: the quantitative investor who wants clean history and reliable backtests, and the AI developer who wants an agent that can reach the markets on its own.</p><p>One note before I start. This is not a ranking of every provider, and it is not a buyer&#8217;s guide. It is the way I think through the decision, with <a href="https://www.alphavantage.co/">Alpha Vantage</a> as the running example, because it offers both a REST API and a mature MCP server over the same data.</p><p>Curious about it? Let&#8217;s get into it.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2><strong>Why does the choice matter more than the code?</strong></h2><p>Connecting to a stock API is usually simple: get a key, call an endpoint, and parse the response. Because that part is so easy, it is tempting to treat the choice of provider as a detail you can settle later.</p><p>In my experience, that is backwards. The real problems rarely show up in the integration code. For example, a free tier capped at a few requests a day stalls a research session halfway through an idea, and a feed with no tool interface leaves you hand-building the adapter an agent needs before it can call anything.</p><p>None of these are coding problems. Each one traces back to the provider you chose. That is why I spend more time choosing a provider than integrating one. Before judging one, though, it helps to be clear on what a stock API actually gives you.</p><div><hr></div><h2><strong>What is a stock API?</strong></h2><p>A stock API is an HTTP service that returns structured market data, usually as JSON or CSV. You send parameters such as a ticker, date range, and interval, and receive data in a predictable format.</p><p>Unlike a finance website, an API is built for repeatable access. The same request can run across hundreds or thousands of tickers while returning the same structure each time. That consistency makes it useful for backtests, dashboards, and other automated workflows.</p><p>There are two common callers. Traditional code calls the endpoint and parses the response. An AI agent can instead call the API through a tool such as an MCP server. The data source is the same; the access layer is different.</p><div><hr></div><h2><strong>How do you reach the data: REST requests and the MCP layer?</strong></h2><p>There are two ways to reach the data. The first is the REST request, the route a traditional integration uses.</p><p>Providers build it two ways: some give each kind of data its own URL, while Alpha Vantage keeps a single URL and lets a function parameter choose the operation. A daily price request looks like this:</p><pre><code><span>GET https://www.alphavantage.co/query
    ?function=TIME_SERIES_DAILY
    &amp;symbol=IBM
    &amp;apikey=YOUR_KEY</span></code></pre><p>Only the function value changes from call to call. Swap <code>TIME_SERIES_DAILY</code> for another function and the same URL returns a live quote or a company overview instead. I like this design because one endpoint is simpler to document and maintain.</p><p>The second route is built for the agent, and it uses the Model Context Protocol (MCP), an open standard. The provider runs a server that lists its tools. The agent reads that list with tools/list, then calls the one it needs with tools/call, filling in the arguments from the conversation. The server runs the query and returns the result.</p><p>The exchange looks like this:</p><pre><code><span>// 1. Agent asks what the server can do
{&#8221;method&#8221;: &#8220;tools/list&#8221;}
 
// 2. Server responds with a typed catalog
{
  &#8220;tools&#8221;: [{
    &#8220;name&#8221;: &#8220;get_daily_time_series&#8221;,
    &#8220;description&#8221;: &#8220;Daily OHLCV for a listed equity&#8221;,
    &#8220;inputSchema&#8221;: {
      &#8220;type&#8221;: &#8220;object&#8221;,
      &#8220;properties&#8221;: {
        &#8220;symbol&#8221;: {&#8221;type&#8221;: &#8220;string&#8221;},
        &#8220;outputsize&#8221;: {&#8221;type&#8221;: &#8220;string&#8221;, &#8220;enum&#8221;: [&#8221;compact&#8221;, &#8220;full&#8221;]}
      },
      &#8220;required&#8221;: [&#8221;symbol&#8221;]
    }
  }]
}
 
// 3. Agent calls the tool with arguments the model filled in
{
  &#8220;method&#8221;: &#8220;tools/call&#8221;,
  &#8220;params&#8221;: {
    &#8220;name&#8221;: &#8220;get_daily_time_series&#8221;,
    &#8220;arguments&#8221;: {&#8221;symbol&#8221;: &#8220;IBM&#8221;, &#8220;outputsize&#8221;: &#8220;compact&#8221;}
  }
}</span></code></pre><p><a href="https://www.alphavantage.co/">Alpha Vantage</a> runs an official MCP server with more than 100 tools across nine categories. Each maps to a single REST function, so the same request returns the same data whichever route you take:</p><ul><li><p>Core stocks: quotes and time series</p></li><li><p>Options: real-time chains with Greeks</p></li><li><p>Fundamentals: statements, overviews, and earnings</p></li><li><p>Forex: spot and historical exchange rates</p></li><li><p>Cryptocurrencies: digital asset prices and time series</p></li><li><p>Commodities: oil, metals, and agricultural prices</p></li><li><p>Economic indicators: GDP, CPI, and treasury yields</p></li><li><p>Technical indicators: more than 50 tools from SMA to MACD</p></li><li><p>Alpha Intelligence: news sentiment, earnings transcripts, and insider transactions</p></li></ul><p>It also sits in Anthropic&#8217;s Claude Connectors directory. That means it installs as a tool source across editors and chat apps like Claude and Cursor, instead of being tied to one app.</p><p>Both routes return the same data. The only difference is who makes the call:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_Qnh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F764ac9f2-2f90-4984-98c4-d9bbb4ee6df3_1172x184.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_Qnh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F764ac9f2-2f90-4984-98c4-d9bbb4ee6df3_1172x184.png 424w, https://substackcdn.com/image/fetch/$s_!_Qnh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F764ac9f2-2f90-4984-98c4-d9bbb4ee6df3_1172x184.png 848w, https://substackcdn.com/image/fetch/$s_!_Qnh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F764ac9f2-2f90-4984-98c4-d9bbb4ee6df3_1172x184.png 1272w, https://substackcdn.com/image/fetch/$s_!_Qnh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F764ac9f2-2f90-4984-98c4-d9bbb4ee6df3_1172x184.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_Qnh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F764ac9f2-2f90-4984-98c4-d9bbb4ee6df3_1172x184.png" width="1172" height="184" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/764ac9f2-2f90-4984-98c4-d9bbb4ee6df3_1172x184.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:184,&quot;width&quot;:1172,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!_Qnh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F764ac9f2-2f90-4984-98c4-d9bbb4ee6df3_1172x184.png 424w, https://substackcdn.com/image/fetch/$s_!_Qnh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F764ac9f2-2f90-4984-98c4-d9bbb4ee6df3_1172x184.png 848w, https://substackcdn.com/image/fetch/$s_!_Qnh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F764ac9f2-2f90-4984-98c4-d9bbb4ee6df3_1172x184.png 1272w, https://substackcdn.com/image/fetch/$s_!_Qnh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F764ac9f2-2f90-4984-98c4-d9bbb4ee6df3_1172x184.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Figure 1. Differences between REST request and MCP layer</p><p>A good MCP server now matters as much as a good REST API.</p><p>It used to be enough to check that the REST docs were solid. Now I also ask whether the provider runs an MCP server and whether its tools match the REST API.</p><p>An agent calling a typed tool makes fewer mistakes and burns fewer tokens than one making raw HTTP calls from hand-written prompts. Both routes pull the same data, so the next question is what data is on offer.</p><div><hr></div><h2><strong>Which categories of market data matter?</strong></h2><p>Market data divides into five broad categories, and most full-featured APIs cover a version of each. Know which one your project requires, because that is where a provider&#8217;s limits show first.</p><ol><li><p>Quotes and real-time prices are the simplest: what a stock trades at right now. Free tiers usually delay that by about fifteen minutes, which is fine for some jobs and useless for others. A live feed costs more, because it is licensed and needs a broker or exchange agreement that most retail APIs skip.</p></li><li><p>Next comes intraday and historical time series: the open, high, low, close, and volume at intervals from a minute to a month. This is the backbone of backtesting, and the one I check hardest, because a strategy is only as reliable as its history.</p></li><li><p>Fundamentals cover the financial statements and valuation ratios behind screeners and valuation models. They matter the moment a strategy depends on more than price.</p></li><li><p>Then there are technical indicators like moving averages and RSI. Alpha Vantage alone exposes more than fifty as ready-made tools. Providers differ on delivery: some compute the indicator on the server, so you request it like a quote, while others hand you raw prices to compute yourself with TA-Lib or pandas-ta.</p></li><li><p>Last is news, macro, and alternative data: headlines, sentiment scores, and economic releases. It is far less standardized than price data, so it is usually the first thing a smaller provider drops.</p></li></ol><p>Think what you need, and the provider you might want to use becomes clearer.</p><div><hr></div><h2><strong>How do you call a stock API the right way?</strong></h2><p>A reliable traditional integration can still be concise. Here is one:</p><pre><code><span>import requests
 
API_KEY = &#8220;YOUR_KEY&#8221;
BASE_URL = &#8220;https://www.alphavantage.co/query&#8221;
 
def get_daily_prices(symbol: str) -&gt; dict:
    params = {
        &#8220;function&#8221;: &#8220;TIME_SERIES_DAILY&#8221;,
        &#8220;symbol&#8221;: symbol,
        &#8220;apikey&#8221;: API_KEY,
        &#8220;outputsize&#8221;: &#8220;compact&#8221;,
    }
    response = requests.get(BASE_URL, params=params, timeout=10)
    response.raise_for_status()
    data = response.json()
 
    if &#8220;Error Message&#8221; in data:
        raise ValueError(f&#8221;Bad request: {data[&#8217;Error Message&#8217;]}&#8221;)
    if &#8220;Note&#8221; in data:
        raise RuntimeError(&#8221;Rate limit hit, back off and retry&#8221;)
 
    return data[&#8221;Time Series (Daily)&#8221;]</span></code></pre><p>Three checks matter here: <code>Error Message</code> catches bad requests, <code>Note</code> catches rate limits, and <code>timeout</code> prevents stalled requests.</p><p>An agent needs the same safeguards, but they move into the MCP server. The server should handle errors and rate limits clearly, and cache repeated requests when appropriate. That is the difference between an MCP server that exists and one that is production-ready.</p><div><hr></div><h2><strong>What does production-ready actually mean?</strong></h2><p>Production-ready comes down to six things: coverage, data quality, latency, rate limits, documentation and licensing, and AI-readiness.</p><ol><li><p>Coverage means whether the provider supports the tickers, exchanges, and asset classes your strategy actually trades. Broad US coverage, for example, says little about another market.</p></li><li><p>Data quality is less visible. Adjusted prices matter because splits can distort raw series. Historical coverage matters too: missing delisted companies introduce survivorship bias, while gaps in thinly traded tickers can distort a backtest.</p></li><li><p>Latency depends on the strategy. End-of-day data may be enough for daily rebalancing, but not for market making. The question is whether the feed is fast enough for the decision it supports.</p></li><li><p>Rate limits determine how much data you can realistically pull. A low per-minute or daily cap can stop a job before it finishes, while bulk endpoints can reduce the number of calls needed.</p></li><li><p>Documentation should explain adjustments, coverage, and edge cases. Licensing matters just as much: personal and commercial use often come with different terms.</p></li><li><p>AI-readiness adds another layer. I look for an MCP server, how much of the REST API it exposes, OAuth support, and compatibility with tools such as Claude, Cursor, and VS Code. Without MCP support, teams need to build and maintain the adapter themselves.</p></li></ol><p>Those six checks narrow the provider choice quickly.</p><div><hr></div><h2><strong>How do you choose a stock data API?</strong></h2><p>When choosing a provider, I use five questions:</p><ul><li><p>What data do I need: quotes, historical prices, fundamentals, indicators, news, or a mix?</p></li><li><p>How fresh must it be: real-time, delayed, or end-of-day?</p></li><li><p>What is my request volume?</p></li><li><p>Is the use personal or commercial?</p></li><li><p>Will an AI agent consume the data?</p></li></ul><p>The last question changes the requirements. If an agent is involved, MCP support becomes part of the evaluation.</p><p>Before committing, test the free tier with your actual tickers and expected request volume. For agent use cases, test the MCP tools separately from the REST API. A good API does not automatically mean a good MCP implementation.</p><p>Applying these questions to the provider used throughout this article shows where it fits.</p><h2><strong>Where does Alpha Vantage fit?</strong></h2><p>Since I have leaned on Alpha Vantage throughout, it is fair to say where it fits and where it does not. As a best-overall pick, its home is quantitative research and agentic AI work, and because its REST API and MCP server draw on the same data, a research team and an AI team can share one provider instead of maintaining two integrations that drift apart.</p><p>That shared foundation is the one thing I would want an AI developer to take away. One integration serves both the backtests and the agents. The historical series a notebook pulls today is the same tool an agent calls next week, when a user asks in plain language instead of writing a query.</p><p>It fits less well for ultra-low-latency trading, and that limit is worth stating plainly. Nothing returned through a shared REST or MCP layer will match a co-located feed. A strategy built on microsecond execution needs a co-located or institutional feed like Bloomberg, or a direct exchange line. No amount of MCP support closes that gap.</p><div><hr></div><h2><strong>The takeaway</strong></h2><p>The main lesson is simple: <strong>the provider creates most of the long-term constraints, not the integration.</strong></p><p>Before building, check the full contract around the data: coverage, quality, rate limits, licensing, pricing, and AI support. A REST client can be rewritten. An MCP adapter can be replaced. Swapping the provider underneath a working system is much harder.</p><p>That is why I would rather spend more time evaluating the provider upfront than discover its limits after the backtests, dashboards, or agents already depend on it.</p><p>For me, that is the real definition of production-ready: not just an API that works, but a provider whose limits are clear enough to build around.</p><p>I would be interested to hear what you check before committing to a market data provider, especially the checks that have caught problems early.</p>]]></content:encoded></item><item><title><![CDATA[Manager Memo: Does Your Team Still Need SQL when AI writes it?]]></title><description><![CDATA[For managers evaluating AI-assisted analytics and data-team operating models]]></description><link>https://www.nb-data.com/p/manager-memo-does-your-team-still</link><guid isPermaLink="false">https://www.nb-data.com/p/manager-memo-does-your-team-still</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Sun, 19 Jul 2026 06:54:09 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!wk-F!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f597e87-e1cd-44c8-8ff2-8cbca3089f98_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wk-F!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f597e87-e1cd-44c8-8ff2-8cbca3089f98_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wk-F!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f597e87-e1cd-44c8-8ff2-8cbca3089f98_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!wk-F!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f597e87-e1cd-44c8-8ff2-8cbca3089f98_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!wk-F!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f597e87-e1cd-44c8-8ff2-8cbca3089f98_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!wk-F!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f597e87-e1cd-44c8-8ff2-8cbca3089f98_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wk-F!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f597e87-e1cd-44c8-8ff2-8cbca3089f98_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3f597e87-e1cd-44c8-8ff2-8cbca3089f98_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1819830,&quot;alt&quot;:&quot;Manager Memo: Does Your Team Still Need SQL when AI writes it?&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/207628602?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f597e87-e1cd-44c8-8ff2-8cbca3089f98_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Manager Memo: Does Your Team Still Need SQL when AI writes it?" title="Manager Memo: Does Your Team Still Need SQL when AI writes it?" srcset="https://substackcdn.com/image/fetch/$s_!wk-F!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f597e87-e1cd-44c8-8ff2-8cbca3089f98_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!wk-F!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f597e87-e1cd-44c8-8ff2-8cbca3089f98_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!wk-F!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f597e87-e1cd-44c8-8ff2-8cbca3089f98_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!wk-F!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3f597e87-e1cd-44c8-8ff2-8cbca3089f98_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h1 style="text-align: justify;">The short answer</h1>
      <p>
          <a href="https://www.nb-data.com/p/manager-memo-does-your-team-still">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Building a Minimal Agent Harness in Python]]></title><description><![CDATA[A beginner-friendly guide to running agent code in another process and stopping it when it takes too long.]]></description><link>https://www.nb-data.com/p/building-a-minimal-agent-harness</link><guid isPermaLink="false">https://www.nb-data.com/p/building-a-minimal-agent-harness</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Thu, 16 Jul 2026 03:29:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!k0rA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d1b7a1f-7527-49ab-9fc1-f12cab91ebe8_1220x813.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!k0rA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d1b7a1f-7527-49ab-9fc1-f12cab91ebe8_1220x813.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!k0rA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d1b7a1f-7527-49ab-9fc1-f12cab91ebe8_1220x813.png 424w, https://substackcdn.com/image/fetch/$s_!k0rA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d1b7a1f-7527-49ab-9fc1-f12cab91ebe8_1220x813.png 848w, https://substackcdn.com/image/fetch/$s_!k0rA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d1b7a1f-7527-49ab-9fc1-f12cab91ebe8_1220x813.png 1272w, https://substackcdn.com/image/fetch/$s_!k0rA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d1b7a1f-7527-49ab-9fc1-f12cab91ebe8_1220x813.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!k0rA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d1b7a1f-7527-49ab-9fc1-f12cab91ebe8_1220x813.png" width="1220" height="813" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1d1b7a1f-7527-49ab-9fc1-f12cab91ebe8_1220x813.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:813,&quot;width&quot;:1220,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Building a Minimal Agent Harness in Python&quot;,&quot;title&quot;:&quot;Diagram showing the application harness in one process and the agent in a separate worker process.&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Building a Minimal Agent Harness in Python" title="Diagram showing the application harness in one process and the agent in a separate worker process." srcset="https://substackcdn.com/image/fetch/$s_!k0rA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d1b7a1f-7527-49ab-9fc1-f12cab91ebe8_1220x813.png 424w, https://substackcdn.com/image/fetch/$s_!k0rA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d1b7a1f-7527-49ab-9fc1-f12cab91ebe8_1220x813.png 848w, https://substackcdn.com/image/fetch/$s_!k0rA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d1b7a1f-7527-49ab-9fc1-f12cab91ebe8_1220x813.png 1272w, https://substackcdn.com/image/fetch/$s_!k0rA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d1b7a1f-7527-49ab-9fc1-f12cab91ebe8_1220x813.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><br>An agent harness is the code around that agent. It starts the work. It waits for the answer and handles failure but the harness does not decide what the answer should be. </p><p>Imagine that your application asks an agent to summarize a document. On a normal run, the agent returns some text and the application continues. But the agent might crash. It might also wait forever for another service.</p><p>A normal Python function call leaves the application waiting at that line until the function returns. If the agent never returns, the code after the call never runs. The application therefore needs a way to stay in control while the agent works.</p><p>This is why, this articlewill build that idea before looking at any files. Once the roles are clear, each part will become a small piece of Python code.</p><p>Curious about it? Let&#8217;s get into it.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h1>The idea before the code</h1><p>The first decision is to run the application and the agent as two separate programs. A running program is called a process. The operating system gives each process its own memory and can stop one without automatically stopping the other.</p><p>The application stays in the first process. It starts a second process when it has work for the agent. We call the second process the worker because its job is to perform one piece of agent work.</p><p>The first process now has another role. It starts the worker and watches for the result. We call it the supervisor because it can stop a failed worker and start a replacement. Worker and supervisor are names for these roles. They are not special Python features.</p><p>One request now follows this path:</p><p><span>1. </span>The application asks the harness for an answer.</p><p><span>2. </span>The harness starts a worker and sends it the request.</p><p><span>3. </span>The worker gives the request to the agent code.</p><p><span>4. </span>The worker sends the answer back to the harness.</p><p><span>5. </span>If the answer takes too long, the harness stops the worker and reports an error.</p><p>This time limit is called a timeout. A timeout is the longest period the harness is willing to wait. The harness can enforce it because the harness is still running outside the worker.</p><p>The two-process design keeps the application in control. It also creates the next problem: the two processes no longer share the same memory. They need a way to exchange a request and a reply.</p><div><hr></div><h1>How the two processes exchange a message</h1><p>When two pieces of code run in the same process, one function can pass a Python value directly to another. A dictionary is one kind of Python value. It stores named fields such as id and message. Separate processes cannot share that dictionary as a live value.</p><p>The operating system can connect the processes with a pipe. A pipe is a channel for moving data from one program to another. The harness writes at one end. The worker reads at the other end and uses a second connection for its reply.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ovqb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46662af1-673c-4c33-914a-94f7e7d19393_1247x702.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ovqb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46662af1-673c-4c33-914a-94f7e7d19393_1247x702.png 424w, https://substackcdn.com/image/fetch/$s_!Ovqb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46662af1-673c-4c33-914a-94f7e7d19393_1247x702.png 848w, https://substackcdn.com/image/fetch/$s_!Ovqb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46662af1-673c-4c33-914a-94f7e7d19393_1247x702.png 1272w, https://substackcdn.com/image/fetch/$s_!Ovqb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46662af1-673c-4c33-914a-94f7e7d19393_1247x702.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ovqb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46662af1-673c-4c33-914a-94f7e7d19393_1247x702.png" width="1247" height="702" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/46662af1-673c-4c33-914a-94f7e7d19393_1247x702.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:702,&quot;width&quot;:1247,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Building a Minimal Agent Harness in Python&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Building a Minimal Agent Harness in Python" title="Building a Minimal Agent Harness in Python" srcset="https://substackcdn.com/image/fetch/$s_!Ovqb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46662af1-673c-4c33-914a-94f7e7d19393_1247x702.png 424w, https://substackcdn.com/image/fetch/$s_!Ovqb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46662af1-673c-4c33-914a-94f7e7d19393_1247x702.png 848w, https://substackcdn.com/image/fetch/$s_!Ovqb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46662af1-673c-4c33-914a-94f7e7d19393_1247x702.png 1272w, https://substackcdn.com/image/fetch/$s_!Ovqb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46662af1-673c-4c33-914a-94f7e7d19393_1247x702.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The harness keeps track of the request. The worker runs the agent. Messages move between the processes through pipes.</figcaption></figure></div><p>A pipe carries bytes, which are the raw units a program uses for data. Both programs therefore need a message format that Python can turn into bytes and back again. This example uses JSON, a common text format for named fields and values.</p><p>The example puts one complete JSON message on each line. This format is called JSON Lines. The line break marks the end of one message, so the reader knows when it has received the complete request.</p><p>A request contains an id and a message. The id is a label for the request. The worker copies that label into its reply. The ok field says whether the reply contains a value or an error.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">request {&#8221;id&#8221;: &#8220;1&#8221;, &#8220;message&#8221;: &#8220;hello&#8221;}
reply {&#8221;id&#8221;: &#8220;1&#8221;, &#8220;ok&#8221;: true, &#8220;value&#8221;: &#8220;Agent received: hello&#8221;}
error {&#8221;id&#8221;: &#8220;1&#8221;, &#8220;ok&#8221;: false, &#8220;error&#8221;: &#8220;agent failed&#8221;}</code></pre></div><p>In this beginner version, one worker handles only one request. Matching the id is still useful because it makes the message format clear. It also prepares the design for a later version that keeps workers alive.</p><p>We now have the full idea: two processes and a small message between them. Only now do we need to give the Python pieces filenames.</p><div><hr></div><h1>The four Python files</h1><p>The example separates the design into four files. Each file has one job.</p><ul><li><p><code>agent.py</code> contains the code that produces an answer.</p></li><li><p><code>worker.py</code> receives one request and calls the agent.</p></li><li><p><code>harness.py</code> starts the worker and enforces the timeout.</p></li><li><p><code>main.py</code> represents the application that wants an answer.</p></li></ul><p>The filenames do not define the design. They make the roles easier to see in a small example. A larger application may organize the code differently while keeping the same separation.</p><p>We will build the files in the order that a request uses them. The agent behavior comes first because it is ordinary Python code.</p><h1>1. The agent produces an answer</h1><p>In Python, a class groups related behavior. The Agent class in this example has one method named reply(). A method is a function that belongs to a class. reply() receives a message and returns a short response.</p><p>Keeping the class this small matters. You can call reply() in a basic test without starting the rest of the system. You can also change the response logic without changing how the process is managed.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># agent.py
class Agent:
    def reply(self, message: str) -&gt; str:
        return f"Agent received: {message}"</code></pre></div><p>For now, reply() only shows which message it received. A real agent could call a model or use a tool here. The harness does not need to know how that answer is produced.</p><p>The class accepts a normal Python value. The worker is the piece that turns an incoming JSON message into that method call.</p><h1>2. The worker connects the pipe to the agent</h1><p>A command-line Python program has two standard text streams. Standard input is where text enters the program. Standard output is where the program writes its normal result.</p><p>The harness connects both streams to pipes when it starts the worker. The worker reads one line from standard input and converts the JSON text into a Python dictionary. It takes the message from that dictionary and passes it to Agent.reply().</p><p>If reply() returns normally, the worker creates a successful JSON reply. If Python reports an error, the except block receives that exception and creates an error reply instead. An exception is Python&#8217;s way of stopping the current operation and reporting what went wrong.</p><p>After building the reply, the worker writes one JSON line to standard output. Python may hold output briefly before sending it. flush() makes sure this line is sent at once. The worker then reaches the end of the file and exits.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># worker.py
import json
import sys

from agent import Agent

agent = Agent()
request = json.loads(sys.stdin.readline())

try:
    value = agent.reply(request["message"])
    reply = {
        "id": request["id"],
        "ok": True,
        "value": value,
    }
except Exception as exc:
    reply = {
        "id": request["id"],
        "ok": False,
        "error": str(exc),
    }

sys.stdout.write(json.dumps(reply) + "\n")
sys.stdout.flush()
</code></pre></div><div class="callout-block" data-callout="true"><p>Standard output is reserved<span> </span>The harness expects every line on standard output to contain JSON. A normal print() would add an unexpected line and break the message format. Send logs to standard error, which is a separate stream for diagnostic messages.</p></div><p>The exception path works only when Python gives control back to the worker. An endless loop does not do that. Code that never returns cannot create its own error reply, so the harness must handle that case from outside the worker.</p><h1>3. The harness controls the worker</h1><p>The harness runs in the application process. For each request, it starts one worker and sends one JSON line. It then waits for one reply.</p><p>While it waits, the harness keeps the timeout. If the reply arrives in time, the harness returns the value to the application. If the time limit passes, it ends the worker process and reports an error.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AJ01!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4165b598-6dc8-4945-bb67-76c9da57df29_1247x702.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AJ01!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4165b598-6dc8-4945-bb67-76c9da57df29_1247x702.png 424w, https://substackcdn.com/image/fetch/$s_!AJ01!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4165b598-6dc8-4945-bb67-76c9da57df29_1247x702.png 848w, https://substackcdn.com/image/fetch/$s_!AJ01!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4165b598-6dc8-4945-bb67-76c9da57df29_1247x702.png 1272w, https://substackcdn.com/image/fetch/$s_!AJ01!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4165b598-6dc8-4945-bb67-76c9da57df29_1247x702.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AJ01!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4165b598-6dc8-4945-bb67-76c9da57df29_1247x702.png" width="1247" height="702" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4165b598-6dc8-4945-bb67-76c9da57df29_1247x702.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:702,&quot;width&quot;:1247,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Building a Minimal Agent Harness in Python&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Building a Minimal Agent Harness in Python" title="Building a Minimal Agent Harness in Python" srcset="https://substackcdn.com/image/fetch/$s_!AJ01!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4165b598-6dc8-4945-bb67-76c9da57df29_1247x702.png 424w, https://substackcdn.com/image/fetch/$s_!AJ01!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4165b598-6dc8-4945-bb67-76c9da57df29_1247x702.png 848w, https://substackcdn.com/image/fetch/$s_!AJ01!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4165b598-6dc8-4945-bb67-76c9da57df29_1247x702.png 1272w, https://substackcdn.com/image/fetch/$s_!AJ01!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4165b598-6dc8-4945-bb67-76c9da57df29_1247x702.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The harness waits outside the worker. It can stop the worker when the timeout expires.</figcaption></figure></div><p>The example uses asyncio because starting a process and reading from a pipe both involve waiting. A function written with async def can pause at await until an operation finishes. During that pause, asyncio can run another part of the program.</p><p>asyncio.subprocess.PIPE tells Python to connect the worker&#8217;s standard input and standard output to the harness. Those connections give ask() a place to send the request and receive the reply.</p><p>With those ideas in place, the harness code is easier to read. The ask() function follows the same request path shown earlier.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># harness.py
import asyncio
import json
import sys
import uuid


async def ask(message: str, timeout: float = 5.0) -&gt; str:
    process = await asyncio.create_subprocess_exec(
        sys.executable,
        "worker.py",
        stdin=asyncio.subprocess.PIPE,
        stdout=asyncio.subprocess.PIPE,
    )

    request = {
        "id": uuid.uuid4().hex,
        "message": message,
    }
    data = (json.dumps(request) + "\n").encode()

    process.stdin.write(data)
    await process.stdin.drain()

    try:
        line = await asyncio.wait_for(
            process.stdout.readline(),
            timeout,
        )
    except TimeoutError:
        process.kill()
        await process.wait()
        raise RuntimeError("agent timed out")

    await process.wait()
    if not line:
        raise RuntimeError("worker exited without a reply")

    reply = json.loads(line)
    if reply["id"] != request["id"]:
        raise RuntimeError("received the wrong reply")
    if not reply["ok"]:
        raise RuntimeError(reply["error"])

    return reply["value"]
</code></pre></div><h2>Reading ask() from top to bottom</h2><ol><li><p>Start a new Python process that runs worker.py.</p></li><li><p>Create a request and turn it into one JSON line.</p></li><li><p>Write the line to the worker through standard input.</p></li><li><p>Wait for one reply until the timeout expires.</p></li><li><p>Check the reply and return the value or raise an error.</p></li></ol><p><strong>Starting the worker. </strong>asyncio.create_subprocess_exec() asks the operating system to start another program. sys.executable selects the same Python installation that is running the harness. The next argument tells Python to run worker.py.</p><p><strong>Building the request. </strong>uuid.uuid4().hex creates a text id that is very unlikely to repeat. json.dumps() turns the request dictionary into JSON text. encode() changes that text into bytes for the pipe.</p><p><strong>Sending and reading. </strong>drain() waits until Python has handed the request bytes to the pipe. readline() waits for one complete reply line. json.loads() changes the reply back into a Python dictionary.</p><p><strong>Enforcing the timeout. </strong>asyncio.wait_for() stops waiting when the time limit passes and raises TimeoutError. Stopping the wait does not stop the worker. process.kill() is the line that ends it.</p><p><strong>Cleaning up. </strong>process.wait() confirms that the worker has ended. The remaining checks reject a missing reply or the wrong id. RuntimeError passes those failures back to the application as normal Python exceptions.</p><p>The harness now has everything it needs to control one request. The last file shows what the application sees.</p><h1>4. The application asks for an answer</h1><p>main.py stands in for the rest of the application. It does not open pipes or create JSON. It calls ask() with a message and a time limit, then prints the returned answer.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># main.py
import asyncio

from harness import ask


async def main() -&gt; None:
    answer = await ask("hello", timeout=2.0)
    print(answer)


asyncio.run(main())</code></pre></div><p>Keep the four Python files in the same folder. Run python main.py from that folder. The normal result is Agent received: hello. The worker then closes because its one request is complete.</p><p>You can also test the failure path. Add time.sleep(10) at the start of Agent.reply() and set the timeout in main.py to 0.5 seconds. The harness should end the worker and report agent timed out.</p><p>That test is more than a demonstration of the error message. It shows that the application can recover without the agent&#8217;s help. The timeout works because it belongs to the process that can still act.</p><div><hr></div><h1>What changes in a larger system</h1><p>The one-request version is enough to explain the boundary. A production service may need to reuse workers and handle more than one request at a time. Those changes require more tracking, but they do not change the basic roles.</p><h2>Reusing workers</h2><p>Starting Python for every request adds delay. A longer-running service may keep several workers ready. This fixed group of workers is called a pool.</p><p>Each worker should still handle one request at a time. If four workers are ready, the service can run four requests. A fifth request waits or receives a busy response until a worker is free.</p><h2>Recovering after a failure</h2><p>A worker that crashes should not return to the pool. Replace it with a new process because you cannot know which in-memory values remain usable. The request that was running should receive an error instead of waiting forever.</p><p>A retry means sending the same request again after a failure. A retry can help when the cause was brief, but it needs a limit. Stop after a small number of attempts so one bad request cannot create an endless crash loop.</p><p>A reused worker also needs a state. State means the stage the worker is in now. It may be starting or ready. While a request runs it is busy. Once stopping begins it must not accept more work.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lV_6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c39c91e-5897-4e87-9db2-5ab6781c7343_1247x702.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lV_6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c39c91e-5897-4e87-9db2-5ab6781c7343_1247x702.png 424w, https://substackcdn.com/image/fetch/$s_!lV_6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c39c91e-5897-4e87-9db2-5ab6781c7343_1247x702.png 848w, https://substackcdn.com/image/fetch/$s_!lV_6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c39c91e-5897-4e87-9db2-5ab6781c7343_1247x702.png 1272w, https://substackcdn.com/image/fetch/$s_!lV_6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c39c91e-5897-4e87-9db2-5ab6781c7343_1247x702.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lV_6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c39c91e-5897-4e87-9db2-5ab6781c7343_1247x702.png" width="1247" height="702" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9c39c91e-5897-4e87-9db2-5ab6781c7343_1247x702.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:702,&quot;width&quot;:1247,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Building a Minimal Agent Harness in Python&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Building a Minimal Agent Harness in Python" title="Building a Minimal Agent Harness in Python" srcset="https://substackcdn.com/image/fetch/$s_!lV_6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c39c91e-5897-4e87-9db2-5ab6781c7343_1247x702.png 424w, https://substackcdn.com/image/fetch/$s_!lV_6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c39c91e-5897-4e87-9db2-5ab6781c7343_1247x702.png 848w, https://substackcdn.com/image/fetch/$s_!lV_6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c39c91e-5897-4e87-9db2-5ab6781c7343_1247x702.png 1272w, https://substackcdn.com/image/fetch/$s_!lV_6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c39c91e-5897-4e87-9db2-5ab6781c7343_1247x702.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">A reused worker moves through clear states. Its current state decides whether it can accept a request.</figcaption></figure></div><h2>Permissions and untrusted code</h2><p>A separate process isolates memory. It is not a security barrier. By default the worker may still read the application&#8217;s files and use its network access.</p><p>For an agent you trust, give the worker only the settings it needs. Run it with a user account that has limited file and network permissions. This reduces what a mistake can affect.</p><p>For code you do not trust, use a container or another restricted environment. A container can limit which files the program sees and which network connections it may open. Those limits come from the container settings rather than from the Python harness.</p><p>These production controls are easier to add after the one-request design is clear. The same request format can cross into a container or a reused worker.</p><h1>Conclusion</h1><p>An agent harness is the control code around agent behavior. It keeps the application able to respond when agent work crashes or takes too long. The separate worker process gives the harness that control.</p><p>The Python files follow the design rather than defining it. agent.py contains the behavior. worker.py connects that behavior to messages. harness.py owns the process and timeout. main.py only asks for an answer.</p><p>Start with one worker and one request until that flow is reliable. Add pools and retries only when the simpler version is clear. Keep the timeout in the process that can stop the worker.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/building-a-minimal-agent-harness?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/building-a-minimal-agent-harness?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p>]]></content:encoded></item><item><title><![CDATA[Why Valid SQL Returns Wrong Numbers]]></title><description><![CDATA[Three ways a query runs without an error and still returns the wrong number, with the query and the fix for each.]]></description><link>https://www.nb-data.com/p/why-valid-sql-returns-wrong-numbers</link><guid isPermaLink="false">https://www.nb-data.com/p/why-valid-sql-returns-wrong-numbers</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Thu, 09 Jul 2026 14:37:27 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ISen!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9233496d-ee27-4954-9163-7f9c188b8f05_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ISen!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9233496d-ee27-4954-9163-7f9c188b8f05_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ISen!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9233496d-ee27-4954-9163-7f9c188b8f05_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!ISen!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9233496d-ee27-4954-9163-7f9c188b8f05_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!ISen!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9233496d-ee27-4954-9163-7f9c188b8f05_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!ISen!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9233496d-ee27-4954-9163-7f9c188b8f05_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ISen!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9233496d-ee27-4954-9163-7f9c188b8f05_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9233496d-ee27-4954-9163-7f9c188b8f05_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1181779,&quot;alt&quot;:&quot;Why Valid SQL Returns Wrong Numbers&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/206293334?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9233496d-ee27-4954-9163-7f9c188b8f05_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Why Valid SQL Returns Wrong Numbers" title="Why Valid SQL Returns Wrong Numbers" srcset="https://substackcdn.com/image/fetch/$s_!ISen!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9233496d-ee27-4954-9163-7f9c188b8f05_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!ISen!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9233496d-ee27-4954-9163-7f9c188b8f05_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!ISen!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9233496d-ee27-4954-9163-7f9c188b8f05_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!ISen!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9233496d-ee27-4954-9163-7f9c188b8f05_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A query that runs is not necessarily right. Running means the database understood you, nothing more. The syntax can be clean, and the number can still be wrong.</p><p>I have reviewed a lot of SQL, and most wrong numbers come from three places: </p><ul><li><p>A join that inflates a total, </p></li><li><p>A filter that drops rows you meant to keep, and </p></li><li><p>A metric that two people define differently. </p></li></ul><p>All three pass without errors, which is exactly why they were considered correct.</p><p>An agent boosts their frequency by swiftly generating and executing queries, leading to more outputs with less verification. While typing is now a quick and inexpensive step, confirming accuracy still depends on your judgment.</p><p>Below are examples, including the query and the fix, on a small dataset you can try yourself:</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.db-fiddle.com/f/bRv4L2rSCCGEtmUHrNhKsj/0&quot;,&quot;text&quot;:&quot;SQL Fiddle&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.db-fiddle.com/f/bRv4L2rSCCGEtmUHrNhKsj/0"><span>SQL Fiddle</span></a></p><p>Well, curious about it? Let&#8217;s get into it.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2>1. Joins that inflate a total</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!arHR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F730302f4-1372-41a1-adab-8129ff504d06_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!arHR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F730302f4-1372-41a1-adab-8129ff504d06_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!arHR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F730302f4-1372-41a1-adab-8129ff504d06_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!arHR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F730302f4-1372-41a1-adab-8129ff504d06_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!arHR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F730302f4-1372-41a1-adab-8129ff504d06_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!arHR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F730302f4-1372-41a1-adab-8129ff504d06_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/730302f4-1372-41a1-adab-8129ff504d06_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1024660,&quot;alt&quot;:&quot;Why Valid SQL Returns Wrong Numbers&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/206293334?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F730302f4-1372-41a1-adab-8129ff504d06_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Why Valid SQL Returns Wrong Numbers" title="Why Valid SQL Returns Wrong Numbers" srcset="https://substackcdn.com/image/fetch/$s_!arHR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F730302f4-1372-41a1-adab-8129ff504d06_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!arHR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F730302f4-1372-41a1-adab-8129ff504d06_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!arHR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F730302f4-1372-41a1-adab-8129ff504d06_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!arHR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F730302f4-1372-41a1-adab-8129ff504d06_1672x941.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>You are asked for total revenue. You have orders and items, so you join them and add up the amount.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">SELECT SUM(o.amount) AS revenue
FROM orders o
JOIN order_items i ON i.order_id = o.order_id; -- 1140</code></pre></div><p>It returns 1140. The real total is 740, and 1140 is not obviously wrong, so it goes into the report.</p><p>A join repeats each row once for every match on the other side. Order 1 has three items, so the join turns it into three rows, each carrying the order&#8217;s amount of 100. Sum the column, and you add that 100 three times. Order 3, with two items, gets counted twice. Eight orders become eleven rows, and the amount for each order is added once per item.</p><p>Revenue lives at the order level, so add it there without the items:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">SELECT SUM(amount) AS revenue FROM orders; -- 740</code></pre></div><p>If you need the items in a single query, roll them up into one row per order first. Either way, count the rows before and after any join. Eight orders should not become eleven rows unless you meant them to.</p><p>Ask: <strong>Does the total change when you remove the join?</strong> If it does, the join is moving the number, and someone should know why, as that number came out too high. </p><p>The next bug does the opposite and is easier to miss.</p><div><hr></div><h2>2. Filters that drop rows you meant to keep</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_EDa!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F985a5247-0b4d-40d7-b2bc-02c565e116c8_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_EDa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F985a5247-0b4d-40d7-b2bc-02c565e116c8_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!_EDa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F985a5247-0b4d-40d7-b2bc-02c565e116c8_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!_EDa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F985a5247-0b4d-40d7-b2bc-02c565e116c8_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!_EDa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F985a5247-0b4d-40d7-b2bc-02c565e116c8_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_EDa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F985a5247-0b4d-40d7-b2bc-02c565e116c8_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/985a5247-0b4d-40d7-b2bc-02c565e116c8_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:932756,&quot;alt&quot;:&quot;Why Valid SQL Returns Wrong Numbers&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/206293334?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F985a5247-0b4d-40d7-b2bc-02c565e116c8_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Why Valid SQL Returns Wrong Numbers" title="Why Valid SQL Returns Wrong Numbers" srcset="https://substackcdn.com/image/fetch/$s_!_EDa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F985a5247-0b4d-40d7-b2bc-02c565e116c8_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!_EDa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F985a5247-0b4d-40d7-b2bc-02c565e116c8_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!_EDa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F985a5247-0b4d-40d7-b2bc-02c565e116c8_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!_EDa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F985a5247-0b4d-40d7-b2bc-02c565e116c8_1672x941.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>There are eight orders, and one is canceled, so &#8220;not canceled&#8221; should be seven. This returns six:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT COUNT(*) FROM orders
WHERE order_status &lt;&gt; &#8216;canceled&#8217;; -- 6</code></pre></div><p>The missing row is the order with no status. When comparing anything to <code>NULL</code>, SQL returns neither true nor false but unknown, and <span>WHERE</span> keeps only what is true. So <code>NULL &lt;&gt;</code><span> &#8216;canceled&#8217;</span> is unknown; the row fails the test, and it drops out. You never chose to exclude it.</p><p>Dates break the same way, and they are harder to spot. Store timestamps in UTC, filter by a local date, and orders near midnight land on the wrong day. A filter for &#8220;all of January&#8221; in local time loses the orders that UTC has already pushed into February, and nothing warns you.</p><p>The fix is to decide what happens to the missing value yourself, instead of letting the comparison decide:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT COUNT(*) FROM orders
WHERE order_status IS DISTINCT FROM &#8216;canceled&#8217;; -- 7</code></pre></div><p><code>IS DISTINCT FROM</code> compares<code> NULL </code>as a real value, so the row stays. For dates, filter in the zone where the data is stored. In both cases, look at how many rows you kept and dropped before you trust the count.</p><p>Ask: What did the filter keep and drop, and was the missing case handled intentionally?</p><p>The first two bugs were caused by SQL doing something you did not expect. The last one is stranger. The SQL does exactly what each person asked for, and they still disagree.</p><div><hr></div><h2>3. Metrics that two people define differently</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!udEd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d1e355-2fbd-4819-a680-0440ca9f7223_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!udEd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d1e355-2fbd-4819-a680-0440ca9f7223_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!udEd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d1e355-2fbd-4819-a680-0440ca9f7223_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!udEd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d1e355-2fbd-4819-a680-0440ca9f7223_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!udEd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d1e355-2fbd-4819-a680-0440ca9f7223_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!udEd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d1e355-2fbd-4819-a680-0440ca9f7223_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e2d1e355-2fbd-4819-a680-0440ca9f7223_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:937015,&quot;alt&quot;:&quot;Why Valid SQL Returns Wrong Numbers&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/206293334?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d1e355-2fbd-4819-a680-0440ca9f7223_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Why Valid SQL Returns Wrong Numbers" title="Why Valid SQL Returns Wrong Numbers" srcset="https://substackcdn.com/image/fetch/$s_!udEd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d1e355-2fbd-4819-a680-0440ca9f7223_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!udEd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d1e355-2fbd-4819-a680-0440ca9f7223_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!udEd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d1e355-2fbd-4819-a680-0440ca9f7223_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!udEd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2d1e355-2fbd-4819-a680-0440ca9f7223_1672x941.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Two analysts run the same data and report two numbers: 60 percent and 20 percent. Both wrote correct SQL.</p><p>&#8220;Repeat purchase rate&#8221; sounds precise, and it is not. It hides three choices, and the two are made differently. The first counts any customer with two or more orders across all customers:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT ROUND(100.0 * COUNT(*) FILTER (WHERE orders_count &gt;= 2)
 / COUNT(*), 0) AS repeat_rate_pct
FROM (SELECT customer_id, COUNT(*) AS orders_count
 FROM orders GROUP BY customer_id) c; -- 60</code></pre></div><p>Three of the five customers have a second order, so 60 percent. The other analyst set a higher bar: two delivered orders inside 90 days, over everyone who ordered.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">WITH delivered AS (
 SELECT customer_id, order_ts
 FROM orders WHERE order_status = &#8216;delivered&#8217;
),
repeat_customers AS (
 SELECT d1.customer_id
 FROM delivered d1
 JOIN delivered d2
 ON d1.customer_id = d2.customer_id
 AND d2.order_ts &gt; d1.order_ts
 AND d2.order_ts &lt;= d1.order_ts + INTERVAL &#8216;90 days&#8217;
 GROUP BY d1.customer_id
)
SELECT ROUND(100.0 * COUNT(DISTINCT customer_id)
 / (SELECT COUNT(DISTINCT customer_id) FROM orders), 0) AS repeat_rate_pct
FROM repeat_customers; -- 20</code></pre></div><p>Now one customer qualifies. Of the three with two orders, one had a canceled second order, one ordered again more than 90 days later, and one had two delivered orders close together. So 20 percent.</p><p>The data and skills are identical, yet there's a threefold gap, and neither analyst erred. The key differences are three unrecorded aspects: who is considered a customer, what qualifies as a purchase, and the time window used. A tool cannot resolve these because they relate to the business context, not just the database's capabilities.</p><p>The fix is simple but effective: write the metric as a single sentence that includes its denominator and window before touching SQL. If two people write different sentences, that is the real disagreement, and you found it early.</p><p>Ask: What is the definition, in one sentence, including who counts, over what window, and what is counted?</p><div><hr></div><h2>What to check before you trust a number</h2><p>Most wrong numbers fail in one of three places, so those are the three to check.</p><p><strong>The first is the grain:</strong> what one row represents, and whether a join changed it. The second is the filter: which rows it kept, which it dropped, and how it handled the missing values. The third is the definition: what the metric actually means, written as one sentence that two people would agree on.</p><p>This isn't about rushing SQL development now that it's affordable. Instead, focus on identifying critical areas where a seemingly correct query might still fail, and check them up front before outcomes impact decisions. An agent enhances this process by providing reliable results more quickly than manual checks, although it won't notify you if a result is wrong.</p><p>Don&#8217;t forget that we will have our certified SQL courses soon! Meanwhile, you can also visit our previous SQL written courses. </p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;7c196a7f-5ce4-4620-9010-ae96c4d5c07a&quot;,&quot;caption&quot;:&quot;Hi everyone! Cornellius and Josep Ferrer from DataBites here&#128075;.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;lg&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;#1 What is SQL?&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:6000855,&quot;name&quot;:&quot;Cornellius Yudha Wijaya&quot;,&quot;bio&quot;:&quot;Sharing Data Knowledge to improve your values&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/583076b2-657b-44bf-8aa9-9263e5bf04f0_544x544.png&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null},{&quot;id&quot;:132707413,&quot;name&quot;:&quot;Josep Ferrer&quot;,&quot;bio&quot;:&quot;Outstand using data -- Data Science, Design and Tech Tech Writer @KDnuggets @DataCamp &#128073;&#127995;Inquiries in rfeers@gmail.com&quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd196b5a6-59f2-46dd-99b3-e10ab1bbd27d_604x604.png&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-03-13T11:01:38.041Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!XU6w!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf0f3527-6b1c-4b72-9ffe-44727c6bb790_1380x962.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.nb-data.com/p/2-what-is-sql&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:158828431,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:17,&quot;comment_count&quot;:0,&quot;publication_id&quot;:37262,&quot;publication_name&quot;:&quot;Non-Brand Data&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!06DP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0e1cde-d120-4029-8ffd-2a8c7c6e4504_1280x1280.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div>]]></content:encoded></item><item><title><![CDATA[My book is out: Python Data Analysis, Fourth Edition]]></title><description><![CDATA[Co-written with my co-author, and what the year of writing it was like.]]></description><link>https://www.nb-data.com/p/my-book-is-out-python-data-analysis</link><guid isPermaLink="false">https://www.nb-data.com/p/my-book-is-out-python-data-analysis</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Sat, 04 Jul 2026 10:12:57 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!vRNE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fe4b43-3d75-470e-9b4e-0f14fce131ca_1280x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vRNE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fe4b43-3d75-470e-9b4e-0f14fce131ca_1280x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vRNE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fe4b43-3d75-470e-9b4e-0f14fce131ca_1280x1024.png 424w, https://substackcdn.com/image/fetch/$s_!vRNE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fe4b43-3d75-470e-9b4e-0f14fce131ca_1280x1024.png 848w, https://substackcdn.com/image/fetch/$s_!vRNE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fe4b43-3d75-470e-9b4e-0f14fce131ca_1280x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!vRNE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fe4b43-3d75-470e-9b4e-0f14fce131ca_1280x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vRNE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fe4b43-3d75-470e-9b4e-0f14fce131ca_1280x1024.png" width="1280" height="1024" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/99fe4b43-3d75-470e-9b4e-0f14fce131ca_1280x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1351817,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/205033498?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fe4b43-3d75-470e-9b4e-0f14fce131ca_1280x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vRNE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fe4b43-3d75-470e-9b4e-0f14fce131ca_1280x1024.png 424w, https://substackcdn.com/image/fetch/$s_!vRNE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fe4b43-3d75-470e-9b4e-0f14fce131ca_1280x1024.png 848w, https://substackcdn.com/image/fetch/$s_!vRNE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fe4b43-3d75-470e-9b4e-0f14fce131ca_1280x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!vRNE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fe4b43-3d75-470e-9b4e-0f14fce131ca_1280x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Python Data Analysis, Fourth Edition, which I wrote with my co-author, is out today. You can get it here: </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://packt.link/MhuEn&quot;,&quot;text&quot;:&quot;Book Link&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://packt.link/MhuEn"><span>Book Link</span></a></p><p>Before I describe what&#8217;s in it, I want to write down what the year of working on it was like.</p><h2>The work</h2><p>My co-author and I started this over a year ago. I&#8217;ve worked in data for years and I know the material, so I expected the writing to be straightforward. It wasn&#8217;t.</p><p>Knowing something and explaining it in a book are different jobs. Every example has to run. Every explanation has to make sense to someone reading it for the first time. And you have to keep doing that, chapter after chapter, for months. This book has 17 of them.</p><h2>Writing with anxiety</h2><p>I&#8217;ll be honest about this part. There were stretches of the year when my anxiety was high. Some days, opening the manuscript felt harder than it should have.</p><p>What worked was not waiting to feel better before writing. On bad days I wrote a paragraph, sometimes a bad one, and fixed it later. The manuscript kept moving, and imperfect pages slowly became a book.</p><p>If you&#8217;re building something while dealing with your own anxiety, this is the only advice I have: <strong>keep the work moving, even in small pieces.</strong></p><h2>The collaboration</h2><p>The other thing that carried this book was working with a co-author. When one of us was stuck, the other pushed forward. When one of us wrote something unclear, the other caught it. We each brought different expertise, and the book shows it.</p><h2>What&#8217;s in the book</h2><p>The full title is <strong>Python Data Analysis: Master Python Analytics with Machine Learning, Deep Learning, GenAI, LLMs, and Data Engineering.</strong></p><p>Modern data analysis goes beyond cleaning and visualizing data. Practitioners today build scalable pipelines, apply machine learning, work with text and image data, and use Generative AI and LLMs. Rather than focus on a single library, the book covers the whole workflow:</p><ul><li><p>Foundations: Python libraries, NumPy, pandas, statistics, linear algebra, and visualization</p></li><li><p>Data work: retrieving, processing, storing, and cleaning messy data</p></li><li><p>Time-series analysis, forecasting, and signal processing</p></li><li><p>Supervised and unsupervised learning: regression, classification, clustering, dimensionality reduction, and anomaly detection</p></li><li><p>Ensemble methods, neural networks, and deep learning</p></li><li><p>Text and image analytics, including NLP and sentiment analysis</p></li><li><p>LLMs and Generative AI</p></li><li><p>Scaling with Dask, Modin, Ray, and PySpark</p></li></ul><p>By the end, you can build end-to-end data analysis pipelines and apply modern data science and AI techniques to real problems.</p><p>It&#8217;s written for data analysts, data scientists, business analysts, statisticians, and students. You&#8217;ll need basic math and working knowledge of Python.</p><p>Thank you</p><p>Get the book here: </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://packt.link/MhuEn&quot;,&quot;text&quot;:&quot;Book Link&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://packt.link/MhuEn"><span>Book Link</span></a></p><p>If you read it, tell me what you think. I read every reply and comment.</p>]]></content:encoded></item><item><title><![CDATA[Fix it or throw it away: deciding what to do with an AI output]]></title><description><![CDATA[The harder part is knowing which problems you can fix and which ones mean starting over.]]></description><link>https://www.nb-data.com/p/fix-it-or-throw-it-away-deciding</link><guid isPermaLink="false">https://www.nb-data.com/p/fix-it-or-throw-it-away-deciding</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Tue, 30 Jun 2026 15:33:57 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!x66w!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a95ad1b-d3cc-4aff-9a30-fa4cb3738788_1731x909.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!x66w!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a95ad1b-d3cc-4aff-9a30-fa4cb3738788_1731x909.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!x66w!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a95ad1b-d3cc-4aff-9a30-fa4cb3738788_1731x909.png 424w, https://substackcdn.com/image/fetch/$s_!x66w!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a95ad1b-d3cc-4aff-9a30-fa4cb3738788_1731x909.png 848w, https://substackcdn.com/image/fetch/$s_!x66w!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a95ad1b-d3cc-4aff-9a30-fa4cb3738788_1731x909.png 1272w, https://substackcdn.com/image/fetch/$s_!x66w!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a95ad1b-d3cc-4aff-9a30-fa4cb3738788_1731x909.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!x66w!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a95ad1b-d3cc-4aff-9a30-fa4cb3738788_1731x909.png" width="1456" height="765" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8a95ad1b-d3cc-4aff-9a30-fa4cb3738788_1731x909.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:765,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1031328,&quot;alt&quot;:&quot;Fix it or throw it away: deciding what to do with an AI output&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/204222302?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a95ad1b-d3cc-4aff-9a30-fa4cb3738788_1731x909.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Fix it or throw it away: deciding what to do with an AI output" title="Fix it or throw it away: deciding what to do with an AI output" srcset="https://substackcdn.com/image/fetch/$s_!x66w!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a95ad1b-d3cc-4aff-9a30-fa4cb3738788_1731x909.png 424w, https://substackcdn.com/image/fetch/$s_!x66w!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a95ad1b-d3cc-4aff-9a30-fa4cb3738788_1731x909.png 848w, https://substackcdn.com/image/fetch/$s_!x66w!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a95ad1b-d3cc-4aff-9a30-fa4cb3738788_1731x909.png 1272w, https://substackcdn.com/image/fetch/$s_!x66w!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8a95ad1b-d3cc-4aff-9a30-fa4cb3738788_1731x909.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>An AI tool writes a summary of last quarter&#8217;s sales. Most numbers match the source data, but one sentence says a marketing campaign &#8220;drove&#8221; the revenue lift, and nothing supports that. Fixing it takes thirty seconds: soften the wording or cut the sentence.</p><p>Now picture the same summary, just as fluent, except it used the wrong table the whole time. It used gross revenue when the question was about net. Every figure agrees with the others, and every figure is wrong.</p><p>Both drafts look the same on screen. One needs a light edit. The other needs to be deleted. Telling those two cases apart, fast, is the skill. The decision is always one of three: use, revise, or reject. What matters is the test underneath it, because that test is where most reviewers go wrong.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2>The three options</h2><p>Every output you review ends in one of three actions.</p><ul><li><p><strong>Use. </strong>It&#8217;s good enough for its next step, even if some polish is left.</p></li><li><p><strong>Revise. </strong>The foundation is sound, but there are fixable flaws worth correcting.</p></li><li><p><strong>Reject. </strong>The foundation itself is broken, so fixing it means rebuilding from scratch.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nGsu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3565ae0-f667-44e2-8358-7f6bdd170c3c_2172x724.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nGsu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3565ae0-f667-44e2-8358-7f6bdd170c3c_2172x724.png 424w, https://substackcdn.com/image/fetch/$s_!nGsu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3565ae0-f667-44e2-8358-7f6bdd170c3c_2172x724.png 848w, https://substackcdn.com/image/fetch/$s_!nGsu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3565ae0-f667-44e2-8358-7f6bdd170c3c_2172x724.png 1272w, https://substackcdn.com/image/fetch/$s_!nGsu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3565ae0-f667-44e2-8358-7f6bdd170c3c_2172x724.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nGsu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3565ae0-f667-44e2-8358-7f6bdd170c3c_2172x724.png" width="1456" height="485" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b3565ae0-f667-44e2-8358-7f6bdd170c3c_2172x724.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:485,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1035296,&quot;alt&quot;:&quot;Fix it or throw it away: deciding what to do with an AI output&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/204222302?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3565ae0-f667-44e2-8358-7f6bdd170c3c_2172x724.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Fix it or throw it away: deciding what to do with an AI output" title="Fix it or throw it away: deciding what to do with an AI output" srcset="https://substackcdn.com/image/fetch/$s_!nGsu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3565ae0-f667-44e2-8358-7f6bdd170c3c_2172x724.png 424w, https://substackcdn.com/image/fetch/$s_!nGsu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3565ae0-f667-44e2-8358-7f6bdd170c3c_2172x724.png 848w, https://substackcdn.com/image/fetch/$s_!nGsu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3565ae0-f667-44e2-8358-7f6bdd170c3c_2172x724.png 1272w, https://substackcdn.com/image/fetch/$s_!nGsu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3565ae0-f667-44e2-8358-7f6bdd170c3c_2172x724.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>It&#8217;s three actions, not a score. A score tells you how good something looks, not what to do with it. A draft can look like a nine and still belong in the trash, because the one wrong part is what everything else rests on.</p><p>So the question is never &#8220;how good is this?&#8221; It&#8217;s: can the problem be fixed, or does it sink the whole thing?</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/fix-it-or-throw-it-away-deciding?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/fix-it-or-throw-it-away-deciding?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><p></p><div><hr></div><h2>Small flaws and broken foundations</h2><p>Here is the rule the rest of this article depends on.</p><div class="pullquote"><p><em>Revise when the foundation is sound but the work is incomplete. Reject when the foundation itself is wrong.</em></p></div><p>A small flaw is one you can fix without touching the core logic, data, or reasoning. Typos, a missing citation when you know the source, a wrong chart label, a caveat that doesn&#8217;t change the conclusion, awkward variable names. None of these threaten what&#8217;s underneath. You fix them and move on.</p><p>A broken foundation invalidates the whole output. The wrong dataset or metric. A source that doesn&#8217;t say what&#8217;s claimed, or doesn&#8217;t exist. Reasoning you can&#8217;t trace. Code that runs the wrong algorithm or hardcodes a credential. An answer to a different question than the one asked. You can&#8217;t patch these. Every line below the flaw inherits it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ThB0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c4334ea-f5f2-4ab9-b16e-79450f773728_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ThB0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c4334ea-f5f2-4ab9-b16e-79450f773728_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!ThB0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c4334ea-f5f2-4ab9-b16e-79450f773728_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!ThB0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c4334ea-f5f2-4ab9-b16e-79450f773728_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!ThB0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c4334ea-f5f2-4ab9-b16e-79450f773728_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ThB0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c4334ea-f5f2-4ab9-b16e-79450f773728_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8c4334ea-f5f2-4ab9-b16e-79450f773728_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1241858,&quot;alt&quot;:&quot;Fix it or throw it away: deciding what to do with an AI output&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/204222302?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c4334ea-f5f2-4ab9-b16e-79450f773728_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Fix it or throw it away: deciding what to do with an AI output" title="Fix it or throw it away: deciding what to do with an AI output" srcset="https://substackcdn.com/image/fetch/$s_!ThB0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c4334ea-f5f2-4ab9-b16e-79450f773728_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!ThB0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c4334ea-f5f2-4ab9-b16e-79450f773728_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!ThB0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c4334ea-f5f2-4ab9-b16e-79450f773728_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!ThB0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c4334ea-f5f2-4ab9-b16e-79450f773728_1672x941.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong>The trap:</strong> how bad a flaw is has little to do with how big the fix looks. A broken foundation can be a one-character change, like swapping gross for net. A small flaw can mean rewriting three paragraphs. People reach for reject when the fix looks expensive and use when it looks cheap. That&#8217;s backwards. The question isn&#8217;t how much text changes. It&#8217;s whether the wrong part holds up everything else.</p><div><hr></div><h2>Two checks before you score anything</h2><p>You can triage most outputs in under a minute with two questions, before any detailed review.</p><p><strong>First, is there an automatic reject condition?</strong> Invented citations, confidential or out-of-scope data, a policy violation, a badly wrong headline number. If yes, stop and reject. No point scoring the writing on a draft that already failed.</p><p><strong>Second, can you verify it?</strong> Can you re-run the code, recompute the figure, open every cited source, trace the retrieved context? If you can&#8217;t check a claim that matters, don&#8217;t approve it because it reads well. These models produce text that sounds right, which is not the same as being right.</p><p>Only drafts that pass both checks are worth a full use-versus-revise read.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QqF6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2800480e-73ac-4150-a2b1-f3cab536e631_1122x1402.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QqF6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2800480e-73ac-4150-a2b1-f3cab536e631_1122x1402.png 424w, https://substackcdn.com/image/fetch/$s_!QqF6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2800480e-73ac-4150-a2b1-f3cab536e631_1122x1402.png 848w, https://substackcdn.com/image/fetch/$s_!QqF6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2800480e-73ac-4150-a2b1-f3cab536e631_1122x1402.png 1272w, https://substackcdn.com/image/fetch/$s_!QqF6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2800480e-73ac-4150-a2b1-f3cab536e631_1122x1402.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QqF6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2800480e-73ac-4150-a2b1-f3cab536e631_1122x1402.png" width="1122" height="1402" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2800480e-73ac-4150-a2b1-f3cab536e631_1122x1402.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1402,&quot;width&quot;:1122,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:983307,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/204222302?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2800480e-73ac-4150-a2b1-f3cab536e631_1122x1402.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!QqF6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2800480e-73ac-4150-a2b1-f3cab536e631_1122x1402.png 424w, https://substackcdn.com/image/fetch/$s_!QqF6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2800480e-73ac-4150-a2b1-f3cab536e631_1122x1402.png 848w, https://substackcdn.com/image/fetch/$s_!QqF6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2800480e-73ac-4150-a2b1-f3cab536e631_1122x1402.png 1272w, https://substackcdn.com/image/fetch/$s_!QqF6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2800480e-73ac-4150-a2b1-f3cab536e631_1122x1402.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The same logic in code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">The same logic in code:
if has_critical_violation(output):
    decision = "Reject"
elif not is_verifiable(output):
    decision = "Reject"
else:
    decision = assess(output)   # now decide Use vs Revise
</code></pre></div><div><hr></div><h2>What &#8220;good enough&#8221; means</h2><p>Use doesn&#8217;t mean perfect. It means good enough for the next step, and that bar moves with where the output is going. The same draft can be a use as a private note and a revise as a summary you send to a client. Code that&#8217;s fine in a scratch notebook needs real review before production. The bar is highest when an output triggers an action on its own, like sending a notification, because no one stands between it and the result.</p><p>So name the next step before you decide. An output is a use only when no remaining problem is big enough to block that step.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/fix-it-or-throw-it-away-deciding/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/fix-it-or-throw-it-away-deciding/comments"><span>Leave a comment</span></a></p><div><hr></div><h2>Triage by type of output</h2><p>The line between a small flaw and a broken foundation looks different by output type. Concrete triggers help.</p><p><strong>Data analysis. </strong>Use it when the dataset and definitions are right, the numbers reconcile, and the logic matches the question. Revise when a causal claim overreaches or a caveat is missing, since that&#8217;s fixable with a note while the core data and formulas stay sound. Reject when the metric, grain, or population is wrong, or the question was misread, because every conclusion is then suspect. Wrong data, population, or metric is always a reject.</p><p><strong>Code generation. </strong>Use it when it runs, passes tests, follows your conventions, and solves the right problem. Revise when it works but needs error handling, better names, or more test coverage. Reject when it uses the wrong algorithm, violates security, hardcodes secrets, or won&#8217;t run. &#8220;Looks correct&#8221; isn&#8217;t enough; run it before you trust it.</p><p><strong>Retrieved answers. </strong>Use it when every factual claim maps to a retrieved passage and the system says &#8220;I don&#8217;t know&#8221; when evidence is thin. Revise when the right documents were pulled but the wording is off or one relevant passage was missed. Reject when it invents facts, cites wrong or fabricated sources, or answers off-context. Keep retrieval and generation separate: if the context is wrong, reject the answer and fix the retrieval.</p><p><strong>Dashboards and BI. </strong>Use it when the numbers match approved sources, periods and KPIs are consistent, and the interpretation is careful. Revise when the figures are right but the narrative misses a comparison or footnote. Reject when a headline number can&#8217;t be traced or filters mix fiscal periods. One wrong headline number spoils the report.</p><p>The pattern holds across all four. Style and polish are revise work. Broken substance is reject work. When the wrong part is in the foundation, the data, the retrieval, the algorithm, or the question itself, the answer is reject, no matter how clean the surface looks.</p><div><hr></div><h2>The mistake to watch: editing what you should reject</h2><p>The expensive mistake isn&#8217;t rejecting too much. It&#8217;s revising what you should have rejected.</p><p>Each round of edits makes a flawed output look better. The writing tightens, the code reads cleaner, and all of it polishes a wrong foundation into something more convincing and harder to catch. A run of reasonable fixes can keep a big error in place and make it look authoritative.</p><p>If 90% of a report is good but the core assumption is wrong, reject it. If 10% of otherwise-sound code is missing, revise it. The share that changes is a distraction. What matters is whether you&#8217;re polishing something true or dressing up something false.</p><p>A simple check: by the third edit on the same output, stop and ask whether you&#8217;re fixing flaws or hiding one.</p><div><hr></div><h2>Conclusion</h2><p>The framework is small on purpose: three outcomes and one test underneath them. When an AI output lands on your desk, don&#8217;t grade it. Ask whether the thing that&#8217;s wrong sits in the foundation or on the surface.</p><p>Surface flaws are revise work. A broken foundation is a reject, however polished it looks, and no amount of editing will fix it. Run the two gates first, name the next step before you decide, and keep a short trail so the same problem doesn&#8217;t come back.</p><p><strong>Stop asking &#8220;how good is this output?&#8221;</strong> Ask &#8220;can the problem be fixed, or does it sink the whole thing?&#8221; Use, revise, and reject are the three answers, and getting the question right is most of the skill.</p><p></p>]]></content:encoded></item><item><title><![CDATA[Why Most GenAI Workflows Need a Review Loop]]></title><description><![CDATA[A Better Prompt Is Not Enough]]></description><link>https://www.nb-data.com/p/why-most-genai-workflows-need-a-review</link><guid isPermaLink="false">https://www.nb-data.com/p/why-most-genai-workflows-need-a-review</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Wed, 24 Jun 2026 15:09:43 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!pW8I!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce8959d-5645-4a93-bd1f-244333fdd64e_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pW8I!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce8959d-5645-4a93-bd1f-244333fdd64e_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pW8I!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce8959d-5645-4a93-bd1f-244333fdd64e_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!pW8I!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce8959d-5645-4a93-bd1f-244333fdd64e_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!pW8I!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce8959d-5645-4a93-bd1f-244333fdd64e_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!pW8I!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce8959d-5645-4a93-bd1f-244333fdd64e_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pW8I!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce8959d-5645-4a93-bd1f-244333fdd64e_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bce8959d-5645-4a93-bd1f-244333fdd64e_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:919446,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/203414836?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce8959d-5645-4a93-bd1f-244333fdd64e_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!pW8I!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce8959d-5645-4a93-bd1f-244333fdd64e_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!pW8I!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce8959d-5645-4a93-bd1f-244333fdd64e_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!pW8I!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce8959d-5645-4a93-bd1f-244333fdd64e_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!pW8I!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbce8959d-5645-4a93-bd1f-244333fdd64e_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Most teams spend their time improving the prompt. They reword the instructions, add examples, or switch to a stronger model, and the output usually improves. But there is a limit to this. A prompt only produces one attempt. The model writes a draft and stops. It does not tell you whether the draft is correct, and it does not improve on its own the next time you run it.</p><p>In an <a href="https://www.nb-data.com/p/what-most-genai-evaluation-workflows">earlier article</a>, I argued that reliability depends on the entire workflow, not just the final answer, and that you should check the workflow at each step rather than only the end result. Evaluation is useful, but it only measures how good an output is. It does not improve the output. To improve the output, you need a review loop: a repeatable process that checks the result, identifies what went wrong, fixes it, and records what you learned so the next run improves. The rest of this article explains the idea and then builds one in Python that you can run today.</p><blockquote><p><strong>In short<span>,&nbsp;</span></strong><span>evaluation</span> measures how good an output is. A review loop is the process that improves it.</p></blockquote><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2>A prompt cannot check its own work</h2><p>A single prompt is one pass through the model. It cannot check its own work, it does not remember the previous run, and it has no rule for deciding when the task is complete. In practice, the model is finished when it stops producing text, which is not the same as being correct.</p><p>Anthropic describes a more reliable setup called the evaluator-optimizer pattern. One model writes the draft, and a second model grades it against clear criteria. The draft is then revised until it meets those criteria. Anthropic recommends this when you have clear criteria and when revising the output actually improves it, which covers most real work. Its Claude Code guidance makes a related point: if the system cannot check its work against something concrete, such as a test, a schema, or a second reviewer, then deciding whether the work is done becomes a matter of opinion, and a person has to check everything by hand.</p><p>OpenAI gives similar advice. Reliable quality comes from clear goals, test datasets, automated graders, and ongoing evaluation, rather than from a single well-written prompt. Reliability is something you build a process around, not something you reach through wording alone.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/why-most-genai-workflows-need-a-review?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/why-most-genai-workflows-need-a-review?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><div><hr></div><h2>Common ways the first draft goes wrong</h2><p>A draft can read well and still be wrong. This happens because of how the models are trained. Below are three common problems. The model cannot detect any of them on its own, which is why an outside check is needed.</p><ul><li><p><strong>Models tend to guess.<span> </span></strong>They are trained to produce text that sounds plausible, not text that has been verified. <em>TruthfulQA</em> found that models often repeat common misconceptions. OpenAI&#8217;s 2025 research on hallucinations offers one reason: most training rewards confident answers more than admitting uncertainty, so the model learns to guess rather than say it does not know.</p></li><li><p><strong>Models can overlook the middle of a long input.<span> </span></strong>Even with large context windows, they do not treat every part of the input equally. The <em>Lost in the Middle</em> research found that information in the middle of a long input is often underused, while information at the start and end gets more weight. For summaries and retrieval-based systems, an important detail can be present in the input but missing from the output.</p></li><li><p><strong>Models tend to agree with the user.<span> </span></strong>Research on sycophancy found that several assistants often match the user&#8217;s stated opinion, and that people sometimes prefer an agreeable answer to a correct one. This matters at work, where prompts often suggest the answer the writer expects. If you ask whether the data supports your plan, the model is more likely to agree than to disagree.</p></li></ul><blockquote><p><strong><span>Takeaway: </span></strong>These are normal limits of how the models work. A review loop is there to catch the mistakes the model cannot catch on its own.</p></blockquote><div><hr></div><h2>The four-stage review loop</h2><p>The review loop has four stages. It applies to ordinary documents as well as to code.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YhiF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4d57a1-5ee3-4840-bb03-0700bbcb7790_2040x1062.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YhiF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4d57a1-5ee3-4840-bb03-0700bbcb7790_2040x1062.png 424w, https://substackcdn.com/image/fetch/$s_!YhiF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4d57a1-5ee3-4840-bb03-0700bbcb7790_2040x1062.png 848w, https://substackcdn.com/image/fetch/$s_!YhiF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4d57a1-5ee3-4840-bb03-0700bbcb7790_2040x1062.png 1272w, https://substackcdn.com/image/fetch/$s_!YhiF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4d57a1-5ee3-4840-bb03-0700bbcb7790_2040x1062.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YhiF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4d57a1-5ee3-4840-bb03-0700bbcb7790_2040x1062.png" width="1456" height="758" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2d4d57a1-5ee3-4840-bb03-0700bbcb7790_2040x1062.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:758,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:113775,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/203414836?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4d57a1-5ee3-4840-bb03-0700bbcb7790_2040x1062.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YhiF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4d57a1-5ee3-4840-bb03-0700bbcb7790_2040x1062.png 424w, https://substackcdn.com/image/fetch/$s_!YhiF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4d57a1-5ee3-4840-bb03-0700bbcb7790_2040x1062.png 848w, https://substackcdn.com/image/fetch/$s_!YhiF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4d57a1-5ee3-4840-bb03-0700bbcb7790_2040x1062.png 1272w, https://substackcdn.com/image/fetch/$s_!YhiF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4d57a1-5ee3-4840-bb03-0700bbcb7790_2040x1062.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em><span>Figure 1. The review loop. Output that passes goes straight to capture; output that needs work is revised and run again, and each recorded failure raises the baseline for the next run.</span></em></figcaption></figure></div><ol><li><p><strong>Generate.<span> </span></strong>Produce the first draft, however your workflow does it: a prompt chain, retrieval, or tool calls.</p></li><li><p><strong>Review.<span> </span></strong>Check the draft with code first, then with a second model, and send the hard cases to a person. The claims, the structure, the process that produced it, and safety are all fair game.</p></li><li><p><strong>Revise.<span> </span></strong>Fix the problems found in the review. You can fix the output itself, or fix the workflow that produced it. Re-running with feedback fixes the current draft. Fixing the instruction or the retrieval step also improves future drafts, which has a larger effect over time.</p></li><li><p><strong>Capture.<span> </span></strong>This stage is often skipped, but it is what makes the loop worth running. Record each failure in a reusable form, such as a test case, an evaluation example, a rule, or a guardrail. OpenAI recommends expanding your evaluation set using real production data. Anthropic describes the shift from repeatedly fixing the same problem to a system in which each failure becomes a test that prevents it from recurring.</p></li></ol><blockquote><p><strong><span>Takeaway: </span></strong>If you find a failure but do not record it, the same problem can return on a later run.</p></blockquote><div class="directMessage button" data-attrs="{&quot;userId&quot;:6000855,&quot;userName&quot;:&quot;Cornellius Yudha Wijaya&quot;,&quot;canDm&quot;:null,&quot;dmUpgradeOptions&quot;:null,&quot;isEditorNode&quot;:true}" data-component-name="DirectMessageToDOM"></div><div><hr></div><h2>Building it: clean data from a document</h2><p>Here is the loop applied to a common task: pulling fields out of a messy invoice into validated JSON. The output looks fine at a glance, which is exactly when a review loop earns its place. The code is provider-agnostic, so replace <span>call_model</span> with your own client and install <span>jsonschema</span>.</p><p>Start with the input. This is the kind of text a model reads easily but gets subtly wrong:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;b534b819-5dc9-40cf-893d-1ebb43f060b8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">INVOICE    Acme Industrial Supply
Inv #: AC-10827      Date: 03/14/2025
Bill to: Northwind Trading
 
  2 x Steel bracket (SB-12) ............ 24.00
 10 x Hex bolt M8 @ 3.50 ea ........... 35.00
  1 x Safety gloves, pair .............. 12.75
 
Subtotal: 71.75
Tax (8%): 5.74
TOTAL DUE: 77.49</code></pre></div><p>The setup is your model call and a few imports:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c50a08b8-a2d9-4f47-8fa6-eb5cf92946a1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">"""Structured extraction with a review loop (provider-agnostic).
Needs the standard library plus jsonschema (pip install jsonschema).
Replace call_model with your own client (OpenAI, Anthropic, ...).
"""
import json
import re
from datetime import date
from jsonschema import validate, ValidationError
 
 
def call_model(prompt: str) -&gt; str:
    """Send the prompt to your model and return its text reply."""
    raise NotImplementedError</code></pre></div><p>A schema fixes the shape, so structure and types are checked for free:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;b20971ee-5fdd-4a08-93c6-e0776ed76b90&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># The shape we require. The schema checks structure and types for free.
SCHEMA = {
    "type": "object",
    "required": ["invoice_number", "date", "vendor", "customer",
                 "line_items", "subtotal", "tax", "total"],
    "properties": {
        "invoice_number": {"type": "string"},
        "date": {"type": "string"},
        "vendor": {"type": "string"},
        "customer": {"type": "string"},
        "line_items": {"type": "array", "items": {
            "type": "object",
            "required": ["description", "quantity",
                         "unit_price", "amount"],
            "properties": {
                "description": {"type": "string"},
                "quantity": {"type": "number"},
                "unit_price": {"type": "number"},
                "amount": {"type": "number"},
            },
        }},
        "subtotal": {"type": "number"},
        "tax": {"type": "number"},
        "total": {"type": "number"},
    },
}
</code></pre></div><p>Deterministic checks run first because they are cheap and exact. They catch the arithmetic and the invented numbers that a fluent answer hides:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;64f7480a-9ea5-4414-bf0e-2336f913405e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Deterministic checks: cheap, exact, and run first. These catch the
# arithmetic and made-up numbers that a fluent draft hides.
def near(a, b, tol=0.01):
    return abs(a - b) &lt;= tol
 
 
def check_extraction(data, source):
    issues = []
    for item in data["line_items"]:
        if not near(item["quantity"] * item["unit_price"], item["amount"]):
            issues.append("line '%s': qty x price != amount"
                          % item["description"])
    if not near(sum(i["amount"] for i in data["line_items"]),
                data["subtotal"]):
        issues.append("line amounts do not sum to subtotal")
    if not near(data["subtotal"] + data["tax"], data["total"]):
        issues.append("subtotal + tax != total")
    for field in ("invoice_number", "total"):
        if str(data[field]) not in source:
            issues.append("%s '%s' is not in the document"
                          % (field, data[field]))
    try:
        date.fromisoformat(data["date"])
    except ValueError:
        issues.append("date '%s' is not ISO format" % data["date"])
    return issues
</code></pre></div><p>Some mistakes are not arithmetic. A second model called reviews the extraction against the source and catches things like swapped names:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;b3b6cfa3-5386-4e16-addb-f82f70129d07&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Independent reviewer: a second model call catches what code cannot,
# such as a vendor and customer that have been swapped.
JUDGE = (
    "You verify an invoice extraction against the SOURCE.\n"
    "Reply ONLY with JSON: {\"passed\": bool, \"issues\": [string]}.\n"
    "Check that names, descriptions, and roles match the document."
)
 
 
def judge_extraction(source, data):
    prompt = (JUDGE + "\n\nSOURCE:\n" + source
              + "\n\nEXTRACTION:\n" + json.dumps(data, indent=2))
    return json.loads(call_model(prompt))</code></pre></div><p>Capture writes every failure to a file you can replay later:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;174d5b66-c631-4c9c-a8b6-51c793fb51cc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># Capture: every failure becomes a row you can replay later as a test.
def capture(source, data, issues, path="eval_set.jsonl"):
    row = {"source": source, "output": data, "issues": issues}
    with open(path, "a") as handle:
        handle.write(json.dumps(row) + "\n")</code></pre></div><p>The loop puts it together: generate, run the cheap checks, then the judge, capture failures, revise, and stop after a few tries:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bcea2255-cd3f-4f69-b553-1aa620e87f45&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># The loop: generate, review (code, then judge), capture, revise, repeat.
EXTRACT = (
    "Extract this invoice as JSON matching the agreed schema. "
    "Use ISO dates (YYYY-MM-DD) and copy every number exactly.\n\n"
    "INVOICE:\n"
)
 
 
def extract_invoice(source, max_tries=3):
    prompt = EXTRACT + source
    for attempt in range(1, max_tries + 1):
        print("Attempt %d" % attempt)
        try:
            data = json.loads(call_model(prompt))       # generate
            validate(instance=data, schema=SCHEMA)
            issues = check_extraction(data, source)      # review: code
        except (json.JSONDecodeError, ValidationError) as error:
            data, issues = {}, ["invalid output: " + str(error)[:50]]
        print("  code checks:", "FAIL" if issues else "PASS")
 
        if not issues:                                   # review: judge
            verdict = judge_extraction(source, data)
            issues = [] if verdict["passed"] else verdict["issues"]
            print("  judge:", "FAIL" if issues else "PASS")
 
        if not issues:
            print("  accepted")
            return data
 
        for problem in issues:
            print("    - " + problem)
        capture(source, data, issues)                    # capture
        prompt = (EXTRACT + source + "\n\nYour last answer had:\n"
                  + "\n".join("- " + p for p in issues)
                  + "\n\nReturn corrected JSON.")         # revise
 
    raise RuntimeError("still failing; escalate to a human")</code></pre></div><p>Running it on the invoice above produces the trace below. The first draft is fluent but wrong in two different ways:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;fdb82fdd-256e-49fa-be16-17fe16c2904d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Attempt 1
  code checks: FAIL
    - subtotal + tax != total
    - total '77.94' is not in the document
Attempt 2
  code checks: PASS
  judge: FAIL
    - vendor/customer swapped: seller is Acme Industrial Supply
Attempt 3
  code checks: PASS
  judge: PASS
  accepted</code></pre></div><p>Attempt 1 fails the cheap checks: the total does not match subtotal plus tax, and 77.94 never appears in the document. Attempt 2 fixes the math, but the judge notices the vendor and customer are swapped. Attempt 3 passes both and is accepted. These are the failure modes from earlier in action: a confident wrong number, then a confident wrong label, both caught before release. Two failures were written to <code>eval_set.jsonl </code>on the way.</p><p>Those captured failures are not just logs. Replay them as tests, and a bug you have already fixed cannot quietly come back:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f7319622-c1ad-423e-bce7-2c3c13a3575e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># regression.py: replay captured failures; they should pass now.
import json
from rloop import extract_invoice, check_extraction
 
 
def test_known_failures_now_pass():
    for line in open("eval_set.jsonl"):
        case = json.loads(line)
        fixed = extract_invoice(case["source"])
        assert check_extraction(fixed, case["source"]) == []</code></pre></div><div><hr></div><h2>Three kinds of review</h2><p>That example used all three kinds of review, and most real systems need them together.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!APO7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde9b3bd9-afd3-49c6-943d-6ee6a2457ee7_2040x1130.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!APO7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde9b3bd9-afd3-49c6-943d-6ee6a2457ee7_2040x1130.png 424w, https://substackcdn.com/image/fetch/$s_!APO7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde9b3bd9-afd3-49c6-943d-6ee6a2457ee7_2040x1130.png 848w, https://substackcdn.com/image/fetch/$s_!APO7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde9b3bd9-afd3-49c6-943d-6ee6a2457ee7_2040x1130.png 1272w, https://substackcdn.com/image/fetch/$s_!APO7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde9b3bd9-afd3-49c6-943d-6ee6a2457ee7_2040x1130.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!APO7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde9b3bd9-afd3-49c6-943d-6ee6a2457ee7_2040x1130.png" width="1456" height="807" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/de9b3bd9-afd3-49c6-943d-6ee6a2457ee7_2040x1130.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:807,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:179934,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/203414836?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde9b3bd9-afd3-49c6-943d-6ee6a2457ee7_2040x1130.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!APO7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde9b3bd9-afd3-49c6-943d-6ee6a2457ee7_2040x1130.png 424w, https://substackcdn.com/image/fetch/$s_!APO7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde9b3bd9-afd3-49c6-943d-6ee6a2457ee7_2040x1130.png 848w, https://substackcdn.com/image/fetch/$s_!APO7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde9b3bd9-afd3-49c6-943d-6ee6a2457ee7_2040x1130.png 1272w, https://substackcdn.com/image/fetch/$s_!APO7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde9b3bd9-afd3-49c6-943d-6ee6a2457ee7_2040x1130.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em><span>Figure 2. The three kinds of review. Code catches structural errors cheaply, an independent reviewer catches reasoning the author missed, and a person has the final say on high-stakes decisions.</span></em></figcaption></figure></div><p><strong>Code</strong> handled everything with a clear answer: the schema, the arithmetic, the date format, and the check that numbers came from the source. It is fast and exact, and it caught the bad total. Anthropic, OpenAI, and Microsoft all treat these checks as the base of a good pipeline.</p><p><strong>An independent reviewer</strong>, a separate model call with its own rubric, caught the swapped names that code cannot judge. <em>Self-Refine</em>, <em>CRITIC</em>, and <em>Reflexion</em> all report real gains from this kind of separate critique. One caution from my earlier article: a model reviewer has their own biases, so check it against human judgment from time to time rather than trusting it on its own.</p><p><strong>A person with authority</strong> is the last step, for high-stakes or subjective cases. In the code that is raised at the end, when the loop cannot pass on its own, it stops and hands off instead of shipping. NIST&#8217;s Generative AI Profile recommends that pre-deployment testing reach the people who decide what gets released.</p><blockquote><p><strong><span>Takeaway: </span></strong>One layer of review is rarely enough. Using code, an independent reviewer, and a person together covers more types of error.</p></blockquote><div><hr></div><h2>What repeatability actually means</h2><p>It is worth being clear about what repeatability means here. It does not mean the model produces the same text every time, because it is not deterministic. It means the quality stays within a predictable range even as the wording changes. </p><p>The loop does not remove that variation, but it keeps the results within known limits. In practice, this lets you describe the worst case in advance. The schema check will not allow malformed output, the arithmetic check will not allow a total that does not add up, and the grounding check will not allow a number that is not in the source. The wording can vary; those guarantees do not.</p><div><hr></div><h2>Review is becoming a compliance requirement</h2><p>There is also a practical reason beyond quality. Review and oversight are increasingly expected. NIST&#8217;s Generative AI Profile asks teams to verify sources before and after launch, monitor for issues that require human attention, and keep records. Microsoft&#8217;s code of conduct for its AI services requires ongoing testing, feedback channels, and human oversight, and does not permit significant decisions without it. The EU AI Act requires that people be told when they are interacting with AI or reading AI-generated content.</p><p>Several well-known cases show what can happen without review. Each one failed in a different way:</p><ul><li><p><strong>CNET.<span> </span></strong>A review of its AI-written finance articles found that 41 of 77 needed corrections. The articles were published because no one checked them first.</p></li><li><p><strong>Air Canada.<span> </span></strong>A tribunal held the airline responsible after its chatbot described a refund policy that did not exist, and rejected the argument that the chatbot was a separate entity.</p></li><li><p><strong>Avianca.<span> </span></strong>Lawyers were sanctioned for submitting a court filing that cited cases that did not exist.</p></li></ul><p>In each case, the writing looked fine. What was missing was a review step before the work was released.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/why-most-genai-workflows-need-a-review/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/why-most-genai-workflows-need-a-review/comments"><span>Leave a comment</span></a></p><div><hr></div><h2>Conclusion</h2><p>The main point is straightforward. Evaluation tells you how good an output is. A review loop is how you improve it. The four stages, the three kinds of review, and the governance requirements are all parts of that process.</p><p>As the example shows, the loop is mostly ordinary code: a schema, a few exact checks, a second opinion, and a record of past failures. The useful change is to spend less time refining the prompt and more time on the surrounding process: what gets checked, who can reject the output, what counts as a failure, when you rerun it, and how each failure becomes a permanent check. With those pieces in place, generative AI can move from producing one-off drafts to supporting a process you can rely on.</p>]]></content:encoded></item><item><title><![CDATA[Manager Memo: Reviewing GenAI Output Before It Reaches Stakeholders]]></title><description><![CDATA[A guide for managers who approve AI-assisted work: what to check, when to reject, and how to talk to stakeholders about it.]]></description><link>https://www.nb-data.com/p/manager-memo-reviewing-genai-output</link><guid isPermaLink="false">https://www.nb-data.com/p/manager-memo-reviewing-genai-output</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Sat, 20 Jun 2026 03:21:50 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!tase!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5be6e8-9936-41ee-98f2-d7d08a61e791_1550x866.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tase!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5be6e8-9936-41ee-98f2-d7d08a61e791_1550x866.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tase!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5be6e8-9936-41ee-98f2-d7d08a61e791_1550x866.png 424w, https://substackcdn.com/image/fetch/$s_!tase!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5be6e8-9936-41ee-98f2-d7d08a61e791_1550x866.png 848w, https://substackcdn.com/image/fetch/$s_!tase!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5be6e8-9936-41ee-98f2-d7d08a61e791_1550x866.png 1272w, https://substackcdn.com/image/fetch/$s_!tase!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5be6e8-9936-41ee-98f2-d7d08a61e791_1550x866.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tase!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5be6e8-9936-41ee-98f2-d7d08a61e791_1550x866.png" width="1456" height="813" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bf5be6e8-9936-41ee-98f2-d7d08a61e791_1550x866.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:813,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:116875,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/202718894?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5be6e8-9936-41ee-98f2-d7d08a61e791_1550x866.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tase!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5be6e8-9936-41ee-98f2-d7d08a61e791_1550x866.png 424w, https://substackcdn.com/image/fetch/$s_!tase!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5be6e8-9936-41ee-98f2-d7d08a61e791_1550x866.png 848w, https://substackcdn.com/image/fetch/$s_!tase!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5be6e8-9936-41ee-98f2-d7d08a61e791_1550x866.png 1272w, https://substackcdn.com/image/fetch/$s_!tase!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5be6e8-9936-41ee-98f2-d7d08a61e791_1550x866.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="pullquote"><p>The output looks polished. That is the problem. Polished prose hides weak evidence, and the approving manager, not the model, owns what happens next.</p></div><p>You receive a briefing note, a slide deck, or a client-facing summary. Your team tells you AI helped write it. It reads cleanly: the structure is logical, the tone is confident, the citations look authoritative. The temptation is to skim it, nod, and forward it on.</p><p><strong>Resist that. </strong>Fluent prose is not evidence of accurate content. Generative models are optimized to produce text that <em>sounds</em> right, not text that <em>is</em> right, and the gap between the two is exactly where stakeholder-facing work goes wrong. </p><p>This playbook gives you a structured way to close that gap: a three-layer review gate, a use / revise/reject decision rule, the failure modes worth memorizing, and templates you can adopt this week.</p><p>The core principle, drawn from where every major governance framework converges: <strong><span data-color="rgb(10, 30, 60)" style="color: rgb(10, 30, 60);">treat stakeholder-facing GenAI output as a high-variance draft, not as finished work.</span></strong></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p>
      <p>
          <a href="https://www.nb-data.com/p/manager-memo-reviewing-genai-output">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[What Most GenAI Evaluation Workflows Get Wrong]]></title><description><![CDATA[Reliability is not a property of the final answer. It is a property of the whole system that produced it.]]></description><link>https://www.nb-data.com/p/what-most-genai-evaluation-workflows</link><guid isPermaLink="false">https://www.nb-data.com/p/what-most-genai-evaluation-workflows</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Sun, 14 Jun 2026 05:51:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4RDG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ad100a-a6ca-4cd4-b877-2ac68d3b510b_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4RDG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ad100a-a6ca-4cd4-b877-2ac68d3b510b_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4RDG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ad100a-a6ca-4cd4-b877-2ac68d3b510b_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!4RDG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ad100a-a6ca-4cd4-b877-2ac68d3b510b_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!4RDG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ad100a-a6ca-4cd4-b877-2ac68d3b510b_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!4RDG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ad100a-a6ca-4cd4-b877-2ac68d3b510b_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4RDG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ad100a-a6ca-4cd4-b877-2ac68d3b510b_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/75ad100a-a6ca-4cd4-b877-2ac68d3b510b_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1319052,&quot;alt&quot;:&quot;What Most GenAI Evaluation Workflows Get Wrong&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/201945949?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ad100a-a6ca-4cd4-b877-2ac68d3b510b_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="What Most GenAI Evaluation Workflows Get Wrong" title="What Most GenAI Evaluation Workflows Get Wrong" srcset="https://substackcdn.com/image/fetch/$s_!4RDG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ad100a-a6ca-4cd4-b877-2ac68d3b510b_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!4RDG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ad100a-a6ca-4cd4-b877-2ac68d3b510b_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!4RDG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ad100a-a6ca-4cd4-b877-2ac68d3b510b_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!4RDG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ad100a-a6ca-4cd4-b877-2ac68d3b510b_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I keep seeing the same mistake. A team hooks up a model, eyeballs a handful of outputs, decides it looks fine, and ships. Evaluation only enters the picture after something breaks in production, and by then the team is firefighting, bolting on test cases one incident at a time, always a step behind.</p><p>The issue is not that they forgot to evaluate. It is that they put the eval in the wrong place. They scored the final answer without looking at what produced it. But in any system that retrieves documents, routes intents, calls tools, or chains reasoning steps, the answer is the last place a bug shows up&#8212;and almost never where it starts. A bad retrieval, a misrouted query, or a wrong tool argument can all produce something that reads beautifully and is completely wrong.</p><blockquote><p><em>Evaluation belongs inside the workflow, not after it. GenAI evals should look less like one big pass/fail test and more like layered quality gates.</em></p></blockquote><p>This is not just my opinion. Official guidance from OpenAI, Anthropic, Google Cloud, Microsoft Foundry, LangSmith, and NIST all point the same way: evaluate routing, retrieval, tool use, grounding, safety, and drift&#8212;not just the final paragraph.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2>Why the Final Answer Is Usually Not the Root Cause</h2><p>When something goes wrong, the reflex is to blame the model. But in most real-world setups, the model is just one piece of a longer pipeline. There are at least five places where things can quietly go sideways before the answer even gets generated:</p><p>&#8226; Prompt construction and intent routing</p><p>&#8226; Document retrieval and context assembly</p><p>&#8226; Tool selection and parameter extraction</p><p>&#8226; Agent handoff and orchestration logic</p><p>&#8226; Post-processing and output formatting</p><p>Every major platform now says the same thing. OpenAI recommends scoped tests at every stage. Microsoft says you need to evaluate each step, not just the final output. Google separates response evaluation from trajectory evaluation. Ragas breaks RAG scoring into retrieval metrics and generation metrics. The consensus is clear.</p><blockquote><p><em>Outcome-only evaluation is a scoreboard. Workflow-native evaluation is an instrument panel.</em></p></blockquote><p>End-to-end success rates still matter as the top-level KPI&#8212;but they are terrible debugging tools. Anthropic puts it well: agent mistakes compound, so you need to inspect every intermediate step to figure out whether the failure came from the model, the tooling, the harness, or the eval itself.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WFZ9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5317d519-04b8-4c0a-8a45-0c7ca6354724_2172x724.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WFZ9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5317d519-04b8-4c0a-8a45-0c7ca6354724_2172x724.png 424w, https://substackcdn.com/image/fetch/$s_!WFZ9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5317d519-04b8-4c0a-8a45-0c7ca6354724_2172x724.png 848w, https://substackcdn.com/image/fetch/$s_!WFZ9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5317d519-04b8-4c0a-8a45-0c7ca6354724_2172x724.png 1272w, https://substackcdn.com/image/fetch/$s_!WFZ9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5317d519-04b8-4c0a-8a45-0c7ca6354724_2172x724.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WFZ9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5317d519-04b8-4c0a-8a45-0c7ca6354724_2172x724.png" width="1456" height="485" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5317d519-04b8-4c0a-8a45-0c7ca6354724_2172x724.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:485,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1285920,&quot;alt&quot;:&quot;What Most GenAI Evaluation Workflows Get Wrong&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/201945949?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5317d519-04b8-4c0a-8a45-0c7ca6354724_2172x724.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="What Most GenAI Evaluation Workflows Get Wrong" title="What Most GenAI Evaluation Workflows Get Wrong" srcset="https://substackcdn.com/image/fetch/$s_!WFZ9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5317d519-04b8-4c0a-8a45-0c7ca6354724_2172x724.png 424w, https://substackcdn.com/image/fetch/$s_!WFZ9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5317d519-04b8-4c0a-8a45-0c7ca6354724_2172x724.png 848w, https://substackcdn.com/image/fetch/$s_!WFZ9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5317d519-04b8-4c0a-8a45-0c7ca6354724_2172x724.png 1272w, https://substackcdn.com/image/fetch/$s_!WFZ9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5317d519-04b8-4c0a-8a45-0c7ca6354724_2172x724.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Figure 1: How output-only and workflow-native evaluation compare across key dimensions.</em></figcaption></figure></div><div><hr></div><h2>Where Evaluation Actually Belongs</h2><p>The most useful eval setups start by breaking the system into stages and writing checks that match what each stage is supposed to do. Here are the five checkpoints that matter:</p><p>&#8226; <strong>Input &amp; Routing &#8212; </strong>Did we understand the user&#8217;s intent? Did we catch injection attempts? Did we route to the right handler?</p><p>&#8226; <strong>Retrieval &amp; Context &#8212; </strong>Did we pull the right documents? Are they relevant? Is the context complete enough to answer the question?</p><p>&#8226; <strong>Tool Use &amp; Planning &#8212; </strong>Did the agent pick the right tool? Are the arguments valid? Did it follow a reasonable path to get there?</p><p>&#8226; <strong>Generation &#8212; </strong>Is the answer correct, complete, and grounded in the evidence? Is it safe? Does it know when to say &#8220;I don&#8217;t know&#8221;</p><p>&#8226; <strong>Production Monitoring &#8212; </strong>How fast is it? How much does it cost? Is quality drifting? Are users flagging problems?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gvLG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33b01765-49e5-4b1f-a6af-0b2de52177b8_2172x724.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gvLG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33b01765-49e5-4b1f-a6af-0b2de52177b8_2172x724.png 424w, https://substackcdn.com/image/fetch/$s_!gvLG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33b01765-49e5-4b1f-a6af-0b2de52177b8_2172x724.png 848w, https://substackcdn.com/image/fetch/$s_!gvLG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33b01765-49e5-4b1f-a6af-0b2de52177b8_2172x724.png 1272w, https://substackcdn.com/image/fetch/$s_!gvLG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33b01765-49e5-4b1f-a6af-0b2de52177b8_2172x724.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gvLG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33b01765-49e5-4b1f-a6af-0b2de52177b8_2172x724.png" width="1456" height="485" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/33b01765-49e5-4b1f-a6af-0b2de52177b8_2172x724.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:485,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1049427,&quot;alt&quot;:&quot;What Most GenAI Evaluation Workflows Get Wrong&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/201945949?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33b01765-49e5-4b1f-a6af-0b2de52177b8_2172x724.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="What Most GenAI Evaluation Workflows Get Wrong" title="What Most GenAI Evaluation Workflows Get Wrong" srcset="https://substackcdn.com/image/fetch/$s_!gvLG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33b01765-49e5-4b1f-a6af-0b2de52177b8_2172x724.png 424w, https://substackcdn.com/image/fetch/$s_!gvLG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33b01765-49e5-4b1f-a6af-0b2de52177b8_2172x724.png 848w, https://substackcdn.com/image/fetch/$s_!gvLG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33b01765-49e5-4b1f-a6af-0b2de52177b8_2172x724.png 1272w, https://substackcdn.com/image/fetch/$s_!gvLG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33b01765-49e5-4b1f-a6af-0b2de52177b8_2172x724.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Figure 2: Each stage has its own eval checkpoint, with a feedback loop from production back into the test data.</em></figcaption></figure></div><p>If this sounds like a lot of work, it does not have to be, at least not on day one. The practical move is risk-weighted decomposition. Pick the two or three user journeys that matter most, write clear success criteria for each, and start with a small curated dataset. Anthropic, OpenAI, and LangSmith all recommend beginning with 10-20 high-quality examples and growing from real failures. That keeps things lightweight while still making problems visible.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/what-most-genai-evaluation-workflows?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/what-most-genai-evaluation-workflows?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><div><hr></div><h2>Picking Metrics That Match How Things Actually Break</h2><p>The golden rule: use the cheapest reliable check first. Start with deterministic graders: exact match, schema validation, regex, policy checks, citation lookups. Only bring in LLM judges when you need to score something fuzzy like helpfulness or completeness.</p><p>What to track depends on your system type:</p><ul><li><p><strong>RAG systems: </strong>context precision, context recall, grounding, faithfulness. Track retrieval and generation as separate scores.</p></li><li><p><strong>Agent systems: </strong>tool selection accuracy, argument validity, task completion rate, trajectory precision, and recall.</p></li><li><p><strong>Any system: </strong>correctness, completeness, safety, abstention quality. Keep these as separate dimensions&#8212;one overall number hides too much.</p></li></ul><p><strong>A warning about LLM-as-judge: </strong>yes, LLM judges can scale evaluation and hit human-level agreement on some tasks. But research (MT-Bench, the &#8220;Large Language Models are not Fair Evaluators&#8221; paper) shows they are prone to position bias, verbosity bias, and self-enhancement bias. Just swapping the order of two responses can flip the ranking. The fix is not to avoid LLM judges; it is to calibrate them. Use pairwise comparisons, pass/fail rubrics with clear criteria, and periodically check agreement against human labels. Targeted human calibration beats full human scoring.</p><div class="directMessage button" data-attrs="{&quot;userId&quot;:6000855,&quot;userName&quot;:&quot;Cornellius Yudha Wijaya&quot;,&quot;canDm&quot;:null,&quot;dmUpgradeOptions&quot;:null,&quot;isEditorNode&quot;:true}" data-component-name="DirectMessageToDOM"></div><div><hr></div><h2>A Six-Step Playbook for CI and Production</h2><p>Evals that live only in notebooks are evals that rot. Here is how to make them part of your development and deployment loop:</p><ol><li><p><strong>Instrument traces early. </strong>From the first real prototype, capture everything: input, system prompt, routing decision, retrieved docs, tool calls, results, final answer, latency, cost, feedback. Without traces, you can evaluate only the output, not the workflow.</p></li><li><p><strong>Start with a small, sharp dataset. </strong>10-20 curated examples per critical path. Include happy paths, edge cases, known failure modes, and adversarial inputs. Quality beats quantity here.</p></li><li><p><strong>Layer your graders. </strong>Deterministic checks first (schema validation, regex, policy filters). LLM judges second (for nuanced criteria like completeness). Sampled human review third (for calibration and high-stakes cases).</p></li><li><p><strong>Wire evals into CI/CD. </strong>When prompts, retrievers, tool schemas, or orchestration logic change, run component-level and end-to-end evals automatically. Fail the build if pass rates drop.</p></li><li><p><strong>Add online evaluation after deploy. </strong>Score a sample of production traces for quality, safety, and cost. Route low scores and negative feedback into annotation queues. Feed confirmed failures back into your offline dataset.</p></li><li><p><strong>Schedule security scans. </strong>Red-team prompts, poisoned-context tests, and injection probes should run on a regular cadence, not just at deploy time.</p></li></ol><p>This is the pattern you will find across LangSmith, MLflow, Microsoft Foundry, and Promptfoo. It is where evaluation stops being a one-off check and becomes a continuous part of how the system runs.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WAbx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6194ac07-e1cc-482f-9f98-879c7ca6e606_1000x1340.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WAbx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6194ac07-e1cc-482f-9f98-879c7ca6e606_1000x1340.png 424w, https://substackcdn.com/image/fetch/$s_!WAbx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6194ac07-e1cc-482f-9f98-879c7ca6e606_1000x1340.png 848w, https://substackcdn.com/image/fetch/$s_!WAbx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6194ac07-e1cc-482f-9f98-879c7ca6e606_1000x1340.png 1272w, https://substackcdn.com/image/fetch/$s_!WAbx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6194ac07-e1cc-482f-9f98-879c7ca6e606_1000x1340.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WAbx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6194ac07-e1cc-482f-9f98-879c7ca6e606_1000x1340.png" width="1000" height="1340" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6194ac07-e1cc-482f-9f98-879c7ca6e606_1000x1340.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1340,&quot;width&quot;:1000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;What Most GenAI Evaluation Workflows Get Wrong&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="What Most GenAI Evaluation Workflows Get Wrong" title="What Most GenAI Evaluation Workflows Get Wrong" srcset="https://substackcdn.com/image/fetch/$s_!WAbx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6194ac07-e1cc-482f-9f98-879c7ca6e606_1000x1340.png 424w, https://substackcdn.com/image/fetch/$s_!WAbx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6194ac07-e1cc-482f-9f98-879c7ca6e606_1000x1340.png 848w, https://substackcdn.com/image/fetch/$s_!WAbx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6194ac07-e1cc-482f-9f98-879c7ca6e606_1000x1340.png 1272w, https://substackcdn.com/image/fetch/$s_!WAbx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6194ac07-e1cc-482f-9f98-879c7ca6e606_1000x1340.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>Figure 3: A framework-agnostic Python function showing layered evaluation across all pipeline stages.</em></figcaption></figure></div><div><hr></div><h2>What Happens When Evals Only Check the Surface</h2><p><strong>Air Canada&#8217;s chatbot (Moffatt v. Air Canada). </strong>A customer asked about bereavement fares and got incorrect guidance from the airline&#8217;s chatbot. Air Canada tried to distance itself from the bot&#8217;s answer, but the tribunal was not having it&#8212;the chatbot was part of the company&#8217;s website, and the airline was responsible for what it said.</p><p>This was not a hallucination problem. It was a missing-controls problem. A workflow-level eval would have checked chatbot responses against the actual policy documents, flagged contradictions, required evidence for refund-related answers, and routed anything ambiguous to a human. A style-level review of the output would have missed all of this&#8212;the answer probably read just fine.</p><p><strong>Fabricated case law (Mata v. Avianca). </strong>Lawyers submitted court filings with fake case citations generated by ChatGPT. The court sanctioned them, ordered letters of explanation to the judges who were falsely cited, and imposed a $5,000 fine.</p><p>The output sounded professional. The problem is that nobody checked whether the cited cases actually existed. For any system that conducts research or cites sources, evaluation needs to cover source resolution, citation validity, and handling missing evidence. A confident answer with fabricated support should count as a failure, even if it scores well on fluency.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/what-most-genai-evaluation-workflows/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/what-most-genai-evaluation-workflows/comments"><span>Leave a comment</span></a></p><div><hr></div><h2>Conclusion</h2><p>Better prompts and better models help. But what actually makes GenAI systems reliable is treating evaluation as part of the workflow; not something you bolt on at the end. Checkpoints before generation, around tool use, before delivery, and after deployment through tracing and feedback.</p><div class="callout-block" data-callout="true"><p><em>The playbook: pick 3-5 user journeys. Define what success looks like. Build 10-20 test cases. Use cheap graders first. Wire evals into CI. Sample production traces. The tools already exist. The question is whether your team uses them.</em></p></div><p>The remaining question is simple: does your team treat evaluation as something the system does all the time, or something someone does to the system after the fact?</p>]]></content:encoded></item><item><title><![CDATA[From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow]]></title><description><![CDATA[A concise guide to achieve success in your production GenAI generation]]></description><link>https://www.nb-data.com/p/from-prompt-to-reliable-output-a</link><guid isPermaLink="false">https://www.nb-data.com/p/from-prompt-to-reliable-output-a</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Sun, 31 May 2026 07:47:25 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!C4_t!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17ba1e8e-e774-41a5-9a84-1f3ef25ddf9b_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!C4_t!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17ba1e8e-e774-41a5-9a84-1f3ef25ddf9b_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!C4_t!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17ba1e8e-e774-41a5-9a84-1f3ef25ddf9b_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!C4_t!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17ba1e8e-e774-41a5-9a84-1f3ef25ddf9b_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!C4_t!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17ba1e8e-e774-41a5-9a84-1f3ef25ddf9b_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!C4_t!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17ba1e8e-e774-41a5-9a84-1f3ef25ddf9b_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!C4_t!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17ba1e8e-e774-41a5-9a84-1f3ef25ddf9b_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/17ba1e8e-e774-41a5-9a84-1f3ef25ddf9b_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1763111,&quot;alt&quot;:&quot;From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/199950174?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17ba1e8e-e774-41a5-9a84-1f3ef25ddf9b_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow" title="From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow" srcset="https://substackcdn.com/image/fetch/$s_!C4_t!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17ba1e8e-e774-41a5-9a84-1f3ef25ddf9b_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!C4_t!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17ba1e8e-e774-41a5-9a84-1f3ef25ddf9b_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!C4_t!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17ba1e8e-e774-41a5-9a84-1f3ef25ddf9b_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!C4_t!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F17ba1e8e-e774-41a5-9a84-1f3ef25ddf9b_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Prompt engineering is rarely enough to prepare a GenAI system for production.</p><p>While a single prompt can generate a good output in initial testing, deploying it across hundreds of real-world inputs exposes failures like missing details, incorrect facts, and formatting errors.</p><p>To build a production-ready system, you must transition from prompt optimization to systematic evaluation.</p><p><strong>A prompt defines your request; an evaluation workflow defines your standard and proves whether the system meets it.</strong></p><p>Here is a practical workflow to evaluate and improve GenAI applications.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SqQo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25a0c054-22b7-478a-b984-bc90bf81e7bf_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SqQo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25a0c054-22b7-478a-b984-bc90bf81e7bf_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!SqQo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25a0c054-22b7-478a-b984-bc90bf81e7bf_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!SqQo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25a0c054-22b7-478a-b984-bc90bf81e7bf_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!SqQo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25a0c054-22b7-478a-b984-bc90bf81e7bf_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SqQo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25a0c054-22b7-478a-b984-bc90bf81e7bf_1024x1024.png" width="1024" height="1024" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/25a0c054-22b7-478a-b984-bc90bf81e7bf_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:704000,&quot;alt&quot;:&quot;From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/199950174?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25a0c054-22b7-478a-b984-bc90bf81e7bf_1024x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow" title="From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow" srcset="https://substackcdn.com/image/fetch/$s_!SqQo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25a0c054-22b7-478a-b984-bc90bf81e7bf_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!SqQo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25a0c054-22b7-478a-b984-bc90bf81e7bf_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!SqQo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25a0c054-22b7-478a-b984-bc90bf81e7bf_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!SqQo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25a0c054-22b7-478a-b984-bc90bf81e7bf_1024x1024.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h1>1. The Limits of Initial Testing</h1><p>It is easy to mistake a fluent LLM output for a correct one.</p><p>If a response uses a professional tone and has no grammatical errors, we assume it is accurate.</p><p>However, LLMs are non-deterministic, and a prompt that works for one test case can fail on another.</p><p>In production, incorrect outputs carry significant risks&#8212;whether it is a fabricated metric in an executive report or an incorrect policy in a customer support tool.</p><p>Rather than testing under ideal conditions, developers must identify where a system fails under real-world inputs.</p><div><hr></div><h1>2. Why Prompting Alone is Insufficient</h1><p>A well-crafted prompt is not a testing framework.</p><p>Relying entirely on prompts to ensure quality is insufficient for three reasons:</p><ul><li><p><strong>Input Variability:</strong> Real-world queries are often messy, incomplete, or poorly formatted.</p></li><li><p><strong>Model Variability</strong>: Even at temperature 0.0, models can generate slightly different outputs for the same input.</p></li><li><p><strong>System Dependencies</strong>: In complex architectures like Retrieval-Augmented Generation (RAG), the LLM is only one component. A prompt cannot correct a failure in the retrieval step.</p></li></ul><p>A polished output does not guarantee a reliable system.</p><p><strong>Instead of relying solely on prompt adjustments, you need a structured evaluation workflow.</strong></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/from-prompt-to-reliable-output-a?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/from-prompt-to-reliable-output-a?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><div><hr></div><h1>3. The 7-Step Evaluation Workflow</h1><p>To ensure predictable GenAI behavior, teams should implement a repeatable seven-step evaluation workflow.</p><ul><li><p>Step 1: Define the task in operational terms.</p></li><li><p>Step 2: Build a representative evaluation set.</p></li><li><p>Step 3: Break evaluation into specific dimensions.</p></li><li><p>Step 4: Choose the right grading method.</p></li><li><p>Step 5: Log failures and classify the errors.</p></li><li><p>Step 6: Modify one variable at a time.</p></li><li><p>Step 7: Define production thresholds.</p></li></ul><h2><strong>Step 1: Define the task in operational terms</strong></h2><p>You must translate vague requirements into objective criteria. For example, instead of asking for &#8220;a good summary,&#8221; define the exact parameters:</p><ul><li><p><strong>KPI Commentary:</strong> Accurate metrics, no causal claims unless explicitly backed by the data source, a concise tone, and a maximum of 150 words.</p></li><li><p><strong>SQL Explainer:</strong> Explains joins and filters correctly in plain language, linking them to a specific KPI.</p></li><li><p><strong>Customer RAG:</strong> Answers using only the provided context, cites sources, and states &#8220;I do not know&#8221; if context is missing.</p></li></ul><p>Identify the specific tasks, target audience, constraints, and failures that would render an output unusable.</p><h2><strong>Step 2: Build a representative evaluation set</strong></h2><p>Start with a small evaluation set of 10 to 30 examples. A massive evaluation set is difficult to manage early in development. The set must reflect real-world inputs rather than just ideal cases. It should contain:</p><ul><li><p><strong>Standard inputs:</strong> Common queries to test baseline functionality.</p></li><li><p><strong>Complex/Ambiguous inputs:</strong> Requests with mixed sentiments or multi-step instructions.</p></li><li><p><strong>Edge cases:</strong> Inputs with missing context or specific formatting constraints.</p></li><li><p><strong>High-risk inputs:</strong> Scenarios where errors have significant business or legal impacts.</p></li></ul><p>For example, the initial evaluation set might include for customer evaluation could be like below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ta6_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3adcdc60-a092-425e-bedd-2932b35bc875_1165x400.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ta6_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3adcdc60-a092-425e-bedd-2932b35bc875_1165x400.png 424w, https://substackcdn.com/image/fetch/$s_!ta6_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3adcdc60-a092-425e-bedd-2932b35bc875_1165x400.png 848w, https://substackcdn.com/image/fetch/$s_!ta6_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3adcdc60-a092-425e-bedd-2932b35bc875_1165x400.png 1272w, https://substackcdn.com/image/fetch/$s_!ta6_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3adcdc60-a092-425e-bedd-2932b35bc875_1165x400.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ta6_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3adcdc60-a092-425e-bedd-2932b35bc875_1165x400.png" width="1165" height="400" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3adcdc60-a092-425e-bedd-2932b35bc875_1165x400.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:400,&quot;width&quot;:1165,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:59672,&quot;alt&quot;:&quot;From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/199950174?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3adcdc60-a092-425e-bedd-2932b35bc875_1165x400.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow" title="From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow" srcset="https://substackcdn.com/image/fetch/$s_!ta6_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3adcdc60-a092-425e-bedd-2932b35bc875_1165x400.png 424w, https://substackcdn.com/image/fetch/$s_!ta6_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3adcdc60-a092-425e-bedd-2932b35bc875_1165x400.png 848w, https://substackcdn.com/image/fetch/$s_!ta6_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3adcdc60-a092-425e-bedd-2932b35bc875_1165x400.png 1272w, https://substackcdn.com/image/fetch/$s_!ta6_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3adcdc60-a092-425e-bedd-2932b35bc875_1165x400.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2><strong>Step 3: Break evaluation into specific dimensions</strong></h2><p>Avoid grading outputs with a single overall score, as it blends separate failure modes. Instead, assess performance across specific dimensions:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ZHH6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa068874c-d646-4cbe-87e9-1e0a07d0a305_1168x684.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ZHH6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa068874c-d646-4cbe-87e9-1e0a07d0a305_1168x684.png 424w, https://substackcdn.com/image/fetch/$s_!ZHH6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa068874c-d646-4cbe-87e9-1e0a07d0a305_1168x684.png 848w, https://substackcdn.com/image/fetch/$s_!ZHH6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa068874c-d646-4cbe-87e9-1e0a07d0a305_1168x684.png 1272w, https://substackcdn.com/image/fetch/$s_!ZHH6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa068874c-d646-4cbe-87e9-1e0a07d0a305_1168x684.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ZHH6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa068874c-d646-4cbe-87e9-1e0a07d0a305_1168x684.png" width="1168" height="684" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a068874c-d646-4cbe-87e9-1e0a07d0a305_1168x684.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:684,&quot;width&quot;:1168,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:98810,&quot;alt&quot;:&quot;From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/199950174?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa068874c-d646-4cbe-87e9-1e0a07d0a305_1168x684.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow" title="From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow" srcset="https://substackcdn.com/image/fetch/$s_!ZHH6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa068874c-d646-4cbe-87e9-1e0a07d0a305_1168x684.png 424w, https://substackcdn.com/image/fetch/$s_!ZHH6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa068874c-d646-4cbe-87e9-1e0a07d0a305_1168x684.png 848w, https://substackcdn.com/image/fetch/$s_!ZHH6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa068874c-d646-4cbe-87e9-1e0a07d0a305_1168x684.png 1272w, https://substackcdn.com/image/fetch/$s_!ZHH6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa068874c-d646-4cbe-87e9-1e0a07d0a305_1168x684.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Select the dimensions relevant to your application. A RAG system focuses on Grounding and Completeness, while a code generation tool focuses on Task Success and Format Fidelity.</p><h2><strong>Step 4: Choose the right grading method</strong></h2><p>Use the simplest method that provides accurate results. You can grade outputs using four primary approaches:</p><ol><li><p><strong>Rule-Based Checks: </strong>Programmatic, deterministic, and highly reliable. Ideal for formatting and constraints (e.g., verifying JSON schema or character counts).</p></li><li><p><strong>Reference-Based Checks: </strong>Used when there is a ground-truth answer (e.g., comparing classification labels or verifying generated SQL output against a reference database query).</p></li><li><p><strong>LLM-as-a-Judge: </strong>Used for semantic or stylistic dimensions like tone and factual consistency at scale. These require a strict grading rubric and few-shot examples to maintain consistency.</p></li><li><p><strong>Human Review: </strong>Recommended for highly sensitive or high-impact tasks. Spot-checks by domain experts are also used to calibrate and validate automated LLM judges.</p></li></ol><h2><strong>Step 5: Log failures and classify the errors</strong></h2><p>When a test case fails, identify the root cause before changing variables. Failure in a GenAI system does not always stem from the prompt.</p><p>Classify errors into specific categories:</p><ul><li><p><strong>Prompt Issue:</strong> Instructions are vague or contain conflicting constraints.</p></li><li><p><strong>Retrieval Issue:</strong> The context provided to the model is incomplete, irrelevant, or outdated.</p></li><li><p><strong>Data Issue</strong>: The underlying data source contains incorrect or corrupted information.</p></li><li><p><strong>Model Issue:</strong> The model ignores instructions or generates incorrect claims despite correct prompts and context.</p></li><li><p><strong>Requirements Issue:</strong> The operational criteria for the task were poorly defined.</p></li></ul><p>For example, a retrieval failure cannot be fixed by editing the prompt, and a data quality issue cannot be resolved by upgrading the model. Fix the issue at its source.</p><h2><strong>Step 6: Modify one variable at a time</strong></h2><p>When optimizing the system, change only one variable at a time to isolate what improves or degrades performance.</p><p>Follow this process:</p><ol><li><p><strong>Establish a baseline:</strong> Run your current evaluation set.</p></li><li><p><strong>Identify failures:</strong> Audit failed cases to determine the primary error type.</p></li><li><p><strong>Isolate a single change:</strong> Modify exactly one parameter (e.g., update a prompt rule, adjust chunk size, or change the model temperature).</p></li><li><p><strong>Rerun and compare:</strong> Run the evaluation set again and compare results against the baseline to verify improvement.</p></li></ol><h2><strong>Step 7: Define production thresholds</strong></h2><p>Establish clear metric thresholds to determine if the system is ready to deploy. For subjective dimensions, a standard 1-to-5 rubric is useful:</p><ul><li><p>5 &#8212; Accurate, fully grounded in context, and correctly formatted.</p></li><li><p>4 &#8212; High quality; minor stylistic issues but safe to deploy without review.</p></li><li><p>3 &#8212; Generally correct; minor phrasing or formatting issues requiring human oversight.</p></li><li><p>2 &#8212; Significant gaps, unsupported claims, or ignored constraints.</p></li><li><p>1 &#8212; Incorrect, contains major errors, or is structurally broken.</p></li></ul><p>Set your deployment thresholds based on risk. A low-risk internal tool might require a minimum average score of 3.5, whereas a high-risk or external application may require a minimum of 4.5 on all core dimensions and a strict 5.0 for factual grounding.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share Non-Brand Data&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share Non-Brand Data</span></a></p><div><hr></div><h1>4. Case Study: Evaluation in Practice</h1><p>Consider a GenAI assistant designed to summarize raw analyst notes for stakeholders.</p><p><strong>Input Data (Analyst Notes):</strong></p><p>* Metrics: Active customers at 12,400 (up 8% QoQ, down 3% YoY due to a seasonal promotion). Revenue at $4.2M (met target of $4.1M, driven by enterprise renewals).<br>* Churn: Rose to 4.2% in March (up from 3.5% in January). CS team suspects a competitor release.<br>* Next Actions: CS team to contact high-risk renewals; Product team to release an update in June.</p><p><strong>Generated Output (With Errors):</strong></p><p>An LLM generates the following summary:</p><p>&#8220;Q1 was a stellar quarter for the business. Active customers reached 12,400, showing strong growth. Total revenue reached $4.2M, beating our target of $4.1M due to an incredibly successful seasonal marketing campaign. Although churn rose to 4.2%, our proactive CS team has already contacted all high-risk accounts to guarantee renewal.&#8221;</p><p>A structured assessment reveals key discrepancies:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lfZ2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee1c1294-f07d-44f6-a2d3-6ab64d0ebf3b_1170x968.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lfZ2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee1c1294-f07d-44f6-a2d3-6ab64d0ebf3b_1170x968.png 424w, https://substackcdn.com/image/fetch/$s_!lfZ2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee1c1294-f07d-44f6-a2d3-6ab64d0ebf3b_1170x968.png 848w, https://substackcdn.com/image/fetch/$s_!lfZ2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee1c1294-f07d-44f6-a2d3-6ab64d0ebf3b_1170x968.png 1272w, https://substackcdn.com/image/fetch/$s_!lfZ2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee1c1294-f07d-44f6-a2d3-6ab64d0ebf3b_1170x968.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lfZ2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee1c1294-f07d-44f6-a2d3-6ab64d0ebf3b_1170x968.png" width="1170" height="968" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ee1c1294-f07d-44f6-a2d3-6ab64d0ebf3b_1170x968.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:968,&quot;width&quot;:1170,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:98578,&quot;alt&quot;:&quot;From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/199950174?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee1c1294-f07d-44f6-a2d3-6ab64d0ebf3b_1170x968.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow" title="From Prompt to Reliable Output: A Practical GenAI Evaluation Workflow" srcset="https://substackcdn.com/image/fetch/$s_!lfZ2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee1c1294-f07d-44f6-a2d3-6ab64d0ebf3b_1170x968.png 424w, https://substackcdn.com/image/fetch/$s_!lfZ2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee1c1294-f07d-44f6-a2d3-6ab64d0ebf3b_1170x968.png 848w, https://substackcdn.com/image/fetch/$s_!lfZ2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee1c1294-f07d-44f6-a2d3-6ab64d0ebf3b_1170x968.png 1272w, https://substackcdn.com/image/fetch/$s_!lfZ2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee1c1294-f07d-44f6-a2d3-6ab64d0ebf3b_1170x968.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Evaluating these distinct dimensions allows the team to identify exactly where the model failed. The prompt can then be updated with constraints requiring objective reporting and preventing the model from inferring causality.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/from-prompt-to-reliable-output-a/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/from-prompt-to-reliable-output-a/comments"><span>Leave a comment</span></a></p><div><hr></div><h1>5. Why Technical Teams Must Own Evaluation</h1><p>Evaluating software and data models is a fundamental engineering discipline. Teams already possess the core mental models required:</p><ul><li><p>Understanding that single test cases are not representative of overall performance.</p></li><li><p>Distinguishing between individual qualitative examples and statistical evidence.</p></li><li><p>Applying structured metrics like precision, recall, and error distributions to assess behavior.</p></li></ul><p>Deploying GenAI requires applying these same disciplines to unstructured outputs. </p><p>Reliable production systems are built by designing, testing, and verifying performance systematically rather than focusing solely on creative prompt writing.</p><div><hr></div><h1>Conclusion</h1><p>Prompting is a starting point, but systematic evaluation is what makes a system production-ready. By defining tasks operationally, building representative evaluation sets, assessing performance across distinct dimensions, and optimizing variables individually, developers can build dependable GenAI applications.</p>]]></content:encoded></item><item><title><![CDATA[Best MCP Servers for Stock Market Data]]></title><description><![CDATA[The best MCP servers for connecting AI agents to financial workflow]]></description><link>https://www.nb-data.com/p/best-mcp-servers-for-stock-market</link><guid isPermaLink="false">https://www.nb-data.com/p/best-mcp-servers-for-stock-market</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Thu, 21 May 2026 15:58:20 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!QrbF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f5f7c9b-5a53-48f5-b71c-c46bc58678d1_1280x853.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QrbF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f5f7c9b-5a53-48f5-b71c-c46bc58678d1_1280x853.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QrbF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f5f7c9b-5a53-48f5-b71c-c46bc58678d1_1280x853.jpeg 424w, https://substackcdn.com/image/fetch/$s_!QrbF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f5f7c9b-5a53-48f5-b71c-c46bc58678d1_1280x853.jpeg 848w, https://substackcdn.com/image/fetch/$s_!QrbF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f5f7c9b-5a53-48f5-b71c-c46bc58678d1_1280x853.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!QrbF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f5f7c9b-5a53-48f5-b71c-c46bc58678d1_1280x853.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QrbF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f5f7c9b-5a53-48f5-b71c-c46bc58678d1_1280x853.jpeg" width="1280" height="853" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6f5f7c9b-5a53-48f5-b71c-c46bc58678d1_1280x853.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:853,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!QrbF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f5f7c9b-5a53-48f5-b71c-c46bc58678d1_1280x853.jpeg 424w, https://substackcdn.com/image/fetch/$s_!QrbF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f5f7c9b-5a53-48f5-b71c-c46bc58678d1_1280x853.jpeg 848w, https://substackcdn.com/image/fetch/$s_!QrbF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f5f7c9b-5a53-48f5-b71c-c46bc58678d1_1280x853.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!QrbF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f5f7c9b-5a53-48f5-b71c-c46bc58678d1_1280x853.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@yorgosntrahas?utm_source=medium&amp;utm_medium=referral">Yorgos Ntrahas</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure></div><p>AI agents are becoming more useful for financial research, but they are only as reliable as the data they can access.</p><p>A model can summarize earnings, compare stocks, or assist in market monitoring. But without easy access to structured financial data, tasks become stale. Developers may write custom API wrappers or rely on models to interpret data services, which isn&#8217;t ideal given the need for accurate market info.</p><p>This is where <strong>Model Context Protocol (MCP)</strong> becomes useful.</p><p>An MCP server provides an AI agent with a consistent framework for finding tools, calling data services, and interacting reliably with external systems. In stock market scenarios, this enables an agent to access data uniformly, for example, fundamentals, through a structured interface.</p><p>However, not every market data provider is equally suited to MCP-based workflows. Some offer broad asset coverage and agent-native integrations, while others excel at historical data, institutional-grade fundamentals, or enterprise financial analytics. The right choice depends on what you want to build.</p><p>In this article, we compare the best MCP servers for stock market data, focusing on five providers:</p><ol><li><p>Alpha Vantage</p></li><li><p>Nasdaq Data Link</p></li><li><p>Tiingo</p></li><li><p>Intrinio</p></li><li><p>FactSet</p></li></ol><p>Curious about it? Let&#8217;s get into it.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h3>1. Alpha Vantage (Best Overall)</h3><p><a href="http://alphavantage.co">Alpha Vantage</a> is the strongest overall choice for developers who want to connect LLMs and AI agents to stock market data through MCP. Its official MCP server gives agents a structured way to discover financial tools, retrieve the right data, and work with market information more reliably than a custom set of hardcoded API calls.</p><p>This is crucial because financial agents need more than simple price lookups. They may require access to historical OHLCV data, performance comparisons among multiple tickers, or the integration of market information with macroeconomic data. Alpha Vantage distinguishes itself as one of the few providers capable of supporting this wide range of workflows from a single data source. Its extensive coverage&#8202;&#8212;&#8202;including stocks, ETFs, funds, indices, options, forex, commodities, fundamentals, technicals, and economic indicators&#8202;&#8212;&#8202;ensures it remains a leading choice.</p><p>Alpha Vantage stands out for its market data, which is well-suited to serious financial applications and seamless integration with structured information for agents to reason over. The MCP layer enhances usability by exposing tools that an agent can inspect and invoke directly.</p><p>The Alpha Vantage MCP server is easy to implement and integrates with popular agent environments like Claude, Cursor, VS Code, ChatGPT, and others. Developers can connect remotely via a single MCP URL or run locally with uvx, making it perfect for research agents, coding tools, financial dashboards, and prototypes with minimal setup.</p><p>Alpha Vantage is ideal for MCP workflows that go beyond raw data retrieval to analysis. Users can ask an agent to retrieve NVDA&#8217;s recent prices, calculate RSI, summarize trends, or compare quarterly movements across AI stocks, or combine fundamentals and technicals for a market brief. This versatility makes Alpha Vantage the best default for stock market MCP integrations.</p><h4>Quickstart Example</h4><p>For a remote MCP connection:</p><pre><code>https://mcp.alphavantage.co/mcp?apikey=YOUR_API_KEY</code></pre><p>For a local MCP connection:</p><pre><code>uvx marketdata-mcp-server YOUR_API_KEY</code></pre><p>Example Cursor configuration:</p><pre><code>{
  &#8220;mcpServers&#8221;: {
    &#8220;alphavantage&#8221;: {
      &#8220;url&#8221;: &#8220;https://mcp.alphavantage.co/mcp?apikey=YOUR_API_KEY&#8221;
    }
  }
}</code></pre><p>Then, you can ask your agent something like:</p><pre><code>Use Alpha Vantage MCP to get NVDA daily price data for the past month,
calculate RSI, and summarize the trend.</code></pre><h4>Best for</h4><p>Developers and teams that want the most balanced MCP server for financial agents: broad market coverage, strong analytical flexibility, straightforward setup, and enough data depth to support both simple market questions and more serious research workflows.</p><div><hr></div><h3>2. Nasdaq Data Link (Best for Research-Grade Financial Datasets)</h3><p><a href="https://data.nasdaq.com/institutional-investors">Nasdaq Data Link</a> is a strong choice for teams that want an MCP-based financial agent to work with deeper, more research-oriented datasets, rather than focusing only on everyday price lookups. It is more compelling when the workflow depends on structured historical datasets, specialized financial information, and a richer research context.</p><p>This makes it especially useful for financial agents designed to support investment research, economic analysis, strategy exploration, or data-heavy market investigations. Instead of limiting the agent to asking, &#8220;What happened to this ticker today?&#8221;, Nasdaq Data Link is better suited to questions that require more structured context, such as comparing market behavior across time, analyzing broader economic relationships, or working with datasets that go beyond standard price and indicator endpoints.</p><p>The trade-off is that Nasdaq Data Link is not as immediately straightforward for general-purpose MCP stock workflows as Alpha Vantage. Its value is highest when the team already knows the type of dataset it wants to expose to the agent and is building around more deliberate research use cases. For lighter stock analysis, Alpha Vantage will often be faster to adopt. For dataset-centered financial intelligence, Nasdaq Data Link becomes much more attractive.</p><h4>Quickstart Example</h4><p>A fitting example prompt would be:</p><pre><code>Use the Nasdaq Data Link MCP integration to retrieve a relevant historical
financial dataset, summarize the major trend changes over time, and highlight
periods that deserve closer investigation.</code></pre><h4>Best for</h4><p>Developers, analysts, and research teams that want MCP-based financial agents to work with deeper, dataset-oriented market analysis rather than only quotes, indicators, or lightweight stock lookups.</p><div><hr></div><h3>3. Tiingo (Best for Market Data and News-Aware Analysis)</h3><p><a href="https://www.tiingo.com/">Tiingo</a> is a strong option for developers seeking an MCP-based financial workflow that feels more like a research assistant than a simple market quote tool, where it fits well for agents that need to connect stock market data with richer context on what is happening in the market.</p><p>This is especially useful because many financial questions can&#8217;t be answered by price data alone. A stock can move sharply due to earnings, guidance, sector sentiment, or broader news. In those cases, an AI agent is more helpful when working with structured market data to interpret what may drive the movement, rather than just reporting price changes. Research highlights the importance of combining numerical data with textual information, such as news, to create better market intelligence systems.</p><p>Tiingo ranks well for market analysis workflows that require both data retrieval and narrative context. An MCP-connected agent using Tiingo can focus on questions like recent stock movements, company summaries, historical trend comparisons, or creating comprehensive ticker briefings, rather than viewing the market only as a numerical dataset.</p><p>Tiingo&#8217;s main strength isn&#8217;t covering all financial use cases but providing a useful middle ground: more contextual than basic market-data APIs and more approachable than institutional platforms. It&#8217;s ideal for teams building MCP workflows, helping agents move from understanding &#8220;what happened?&#8221; to analyzing &#8220;what happened and what context to consider.&#8221;</p><h4>Quickstart Example</h4><p>A representative prompt can still show the use case clearly:</p><pre><code>Use the Tiingo MCP integration to review TSLA&#8217;s recent price movement,
surface any relevant market or company context, and summarize the key
points in a short investor-style briefing.</code></pre><h4>Best for</h4><p>Developers and small teams building financial agents that need to combine stock market data retrieval with richer interpretive context, especially for market briefings, watchlist assistants, and news-aware stock analysis workflows.</p><div><hr></div><h3>4. Intrinio (Best for Fundamentals-Driven Financial Agents)</h3><p><a href="https://intrinio.com/">Intrinio</a> is best suited for developers building MCP-based financial agents that delve deeper into company fundamentals, financial statements, valuation context, and business performance analysis. It is better positioned for agents who need to explain a company rather than merely describe its stock movement.</p><p>This matters because many useful financial workflows start with questions not centered on price. Investors, analysts, or business users want to know revenue growth, margin changes, debt levels, or company comparison on profitability and valuation. An MCP-connected agent is more valuable when it can retrieve structured company data and craft a clear financial narrative.</p><p>Intrinio&#8217;s MCP article offers a professional, fundamentals-based ranking, not just market prices. It&#8217;s valuable for AI workflows that support deliberate analysis, such as comparing companies, reviewing business quality, identifying financial strengths, and preparing structured research similar to early analyst work.</p><p>This makes Intrinio especially appealing for teams that want to build agents for equity research, corporate analysis, and data-backed investment workflows. The trade-off is that it may be more than necessary for a lightweight ticker-monitoring agent. But when the job is to reason about companies with more financial depth, Intrinio deserves its place high in the ranking.</p><h4>Quickstart Example</h4><p>A representative prompt could be:</p><pre><code>Use the Intrinio MCP integration to analyze MSFT&#8217;s latest financial performance,
summarize revenue growth, operating margin, cash flow trends, and key balance
sheet observations, then explain what stands out for an equity research brief.</code></pre><h4>Best for</h4><p>Developers and teams building financial agents for company fundamentals, equity research, earnings analysis, valuation support, and more structured business-performance summaries.</p><div><hr></div><h3>5. FactSet (Best for Enterprise Financial Intelligence)</h3><p><a href="https://www.factset.com/">FactSet</a> is the top choice for organizations wanting enterprise-grade financial agents based on institutional data, research workflows, and decision-support tools. Unlike other providers, it focuses on powering AI in serious financial environments like investment banking, wealth management, and institutional research.</p><p>FactSet also stands out because of how naturally it fits the broader movement toward AI agents embedded in financial workflows. In February 2026, Anthropic announced new enterprise AI plug-ins developed with partners including FactSet, aimed at work in investment banking, wealth management, and other professional domains. That does not make FactSet a lightweight public MCP server in the same way Alpha Vantage is positioned, but it does strengthen its place in this article as the premium option for teams thinking about agentic finance at an institutional level.</p><p>For an MCP-based stock market article, FactSet works best as the <strong>enterprise intelligence</strong> pick: the provider for teams that care about combining financial data access with mature workflow context, internal research processes, and high-stakes analysis. An agent connected to FactSet-style capabilities could support more advanced tasks such as summarizing company developments, reviewing investment opportunities, generating analyst-style briefs, or helping build pitch materials that rely on trusted financial information rather than generic web retrieval.</p><p>The trade-off is that FactSet is not the most approachable choice for individual developers or smaller prototypes. Its value becomes clearest when the application is part of a broader professional research stack, and the user needs institutional depth, workflow integration, and enterprise credibility. For those use cases, FactSet deserves the final place in this ranking&#8202;&#8212;&#8202;not because it is the easiest option, but because it is one of the most powerful when the objective is serious financial intelligence.</p><h4>Quickstart Example</h4><p>Because FactSet&#8217;s public AI positioning is more <strong>enterprise- and partner-led</strong> than self-serve, I would use an example prompt rather than a generic public MCP connection snippet.</p><pre><code>Use the FactSet-connected financial agent to review a target company,
summarize recent business developments, highlight major financial risks,
and prepare an executive-style research brief for an investment discussion.</code></pre><h4>Best for</h4><p>Enterprise teams, investment professionals, and financial institutions that want AI agents built around institutional-grade research workflows, professional market intelligence, and high-value financial analysis.</p><div><hr></div><h3>Conclusion</h3><p>The best MCP server for stock market data depends on the kind of financial agent you want to build:</p><ul><li><p><strong>Alpha Vantage: </strong>Best overall for broad market coverage and flexible agent workflows</p></li><li><p><strong>Nasdaq Data Link: </strong>Best for research-grade financial datasets</p></li><li><p><strong>Tiingo: </strong>Best for market data with stronger context for briefings and analysis</p></li><li><p><strong>Intrinio: </strong>Best for fundamentals-driven company research</p></li><li><p><strong>FactSet: </strong>Best for enterprise financial intelligence</p></li></ul><p>That&#8217;s all for now. I hope it helps!</p>]]></content:encoded></item><item><title><![CDATA[The GenAI Skill Data Professionals Need Most: Evaluation]]></title><description><![CDATA[Only by evaluating things that we know our system is good.]]></description><link>https://www.nb-data.com/p/the-genai-skill-data-professionals</link><guid isPermaLink="false">https://www.nb-data.com/p/the-genai-skill-data-professionals</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Thu, 14 May 2026 17:01:21 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ugB7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60e5782f-b33b-410e-be3c-78fc501b9eb3_1020x551.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ugB7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60e5782f-b33b-410e-be3c-78fc501b9eb3_1020x551.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ugB7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60e5782f-b33b-410e-be3c-78fc501b9eb3_1020x551.png 424w, https://substackcdn.com/image/fetch/$s_!ugB7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60e5782f-b33b-410e-be3c-78fc501b9eb3_1020x551.png 848w, https://substackcdn.com/image/fetch/$s_!ugB7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60e5782f-b33b-410e-be3c-78fc501b9eb3_1020x551.png 1272w, https://substackcdn.com/image/fetch/$s_!ugB7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60e5782f-b33b-410e-be3c-78fc501b9eb3_1020x551.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ugB7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60e5782f-b33b-410e-be3c-78fc501b9eb3_1020x551.png" width="1020" height="551" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/60e5782f-b33b-410e-be3c-78fc501b9eb3_1020x551.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:551,&quot;width&quot;:1020,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:986860,&quot;alt&quot;:&quot;The GenAI Skill Data Professionals Need Most: Evaluation&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/197661414?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60e5782f-b33b-410e-be3c-78fc501b9eb3_1020x551.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The GenAI Skill Data Professionals Need Most: Evaluation" title="The GenAI Skill Data Professionals Need Most: Evaluation" srcset="https://substackcdn.com/image/fetch/$s_!ugB7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60e5782f-b33b-410e-be3c-78fc501b9eb3_1020x551.png 424w, https://substackcdn.com/image/fetch/$s_!ugB7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60e5782f-b33b-410e-be3c-78fc501b9eb3_1020x551.png 848w, https://substackcdn.com/image/fetch/$s_!ugB7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60e5782f-b33b-410e-be3c-78fc501b9eb3_1020x551.png 1272w, https://substackcdn.com/image/fetch/$s_!ugB7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60e5782f-b33b-410e-be3c-78fc501b9eb3_1020x551.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>GenAI has made output generation cheap.</p><p>A decent prompt can already produce summaries, classifications, SQL explanations, insight drafts, documentation, and stakeholder-facing text.</p><p>The harder part is deciding:</p><ul><li><p>Is the output correct?</p></li><li><p>Is it grounded in the input?</p></li><li><p>Is it consistent across realistic cases?</p></li><li><p>Would I trust it inside an actual workflow?</p></li></ul><p>That is the opening for data professionals. Evaluation turns GenAI from a demo into something that can be used responsibly in real work.</p><p><a href="https://developers.openai.com/api/docs/guides/evaluation-best-practices">OpenAI&#8217;s current eval guidance</a> makes the same distinction: the useful kind of eval is not a public benchmark. It is a task-specific test for the application you are building.</p><p>By the end of this article, you should have a clearer view of what evaluation means in practice, what to test for, and why data professionals are well-positioned to own this skill. </p><p>Curious about it? Let&#8217;s get into it.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><h1>GenAI output is easy. Trusting it is not.</h1><p>Prompting gets attention because it is visible.</p><p>Evaluation matters more because it decides whether the work holds up.</p><p>Most professionals first experience GenAI as a productivity tool: ask, receive, refine. That makes it tempting to treat a fluent output as a good output.</p><p>But GenAI systems are variable.</p><p>The same system can behave differently across prompts, data slices, and edge cases.</p><p>In professional settings, the cost of a wrong answer is higher than the cost of a slightly worse prompt.</p><p>That is why evaluation matters.</p><div><hr></div><h1>What does evaluation actually mean in real work?</h1><p>Evaluation here does not mean comparing GPT-5.5 against Claude on a benchmark.</p><p>It means testing a GenAI workflow against the task&#8217;s standards.</p><p>The standard is to separate broad model benchmarks from specific evaluations you design for your own application. Google&#8217;s evaluation documentation also frames this as a test-driven process: define the task, prepare evaluation data, choose the quality criteria, and inspect results.</p><p>Consider workflows that data professionals might evaluate:</p><ul><li><p>A GenAI assistant that summarizes KPI movements</p></li><li><p>A RAG system answering questions from internal policy documents</p></li><li><p>A classifier that tags customer feedback or support tickets</p></li><li><p>A model that explains SQL output to business users</p></li></ul><p>These are not abstract research problems. They are real tasks that need real testing.</p><div><hr></div><h1>The four questions every GenAI workflow should answer</h1><p>The heart of evaluation is not a long taxonomy of metrics, as it comes down to these four questions:</p><h3><strong>A. Did it complete the task?</strong></h3><p>This means that if the GenAI application can perform the task it was asked to do, such as classifying the ticket into the allowed categories or summarizing the table instead of restating it.</p><p>In technical terms, this is often measured through deterministic checks: regex matching for expected formats, JSON schema validation, or exact-match accuracy against a predefined label set.</p><div class="callout-block" data-callout="true"><p style="text-align: center;"><code>Exact Match = 1 if output matches ground truth, else 0</code></p></div><p>This is the simplest evaluation dimension, but also the most often skipped.</p><h3><strong>B. Is it correct and grounded?</strong></h3><p>Did it stay faithful to the source data, retrieved context, or provided evidence?</p><p>This is especially important for RAG systems, analytical summaries, and policy assistants.</p><p>Basic text overlap metrics such as ROUGE or BLEU are not sufficient here. They check surface-level word similarity but miss meaning.</p><p>A stronger approach is to compare the output and source in the embedding space. The standard metric for that is cosine similarity:</p><div class="callout-block" data-callout="true"><p style="text-align: center;"><code>Cosine Similarity(A, B) = (A &#183; B) / (||A|| &#215; ||B||)</code></p></div><p>A high cosine similarity means the generated text is semantically close to the source. A low score may indicate the model drifted or fabricated information.</p><p>For RAG systems specifically, you also need to check whether the retrieval step actually found the right documents before the model started generating. That is where Recall@K matters:</p><div class="callout-block" data-callout="true"><p style="text-align: center;"><code>Recall@K = |Relevant Documents &#8745; Top-K Retrieved| / |Relevant Documents|</code></p></div><p>If the retriever misses the relevant source, even a perfect generator will produce the wrong answer.</p><p>These are examples of some metrics from the Generative AI metrics.</p><h3><strong>C. Is the quality consistent across realistic cases?</strong></h3><p>A single good example proves very little.</p><p>The system should be tested against various cases, such as:</p><ul><li><p>Easy cases</p></li><li><p>Ambiguous cases</p></li><li><p>Incomplete inputs</p></li><li><p>Edge cases</p></li><li><p>Cases where the correct response is &#8220;not enough information.&#8221;</p></li></ul><p>You already know not to trust a model based on a single clean validation sample.</p><p>GenAI deserves the same discipline. That means building automated test suites that run every time the prompt, model version, or pipeline changes.</p><h3><strong>D. How does it fail?</strong></h3><p>This is the most important professional instinct.</p><p>A workflow that fails clearly is easier to manage than one that sounds confident while being wrong.</p><p>A good evaluation should reveal things such as:</p><ul><li><p>Unsupported claims</p></li><li><p>Fabricated numbers</p></li><li><p>Hidden assumptions</p></li><li><p>Formatting failures</p></li><li><p>Misleading simplifications</p></li></ul><p>This is where logging and tracing tools become important. Capturing inputs and outputs systematically lets you identify failure patterns instead of guessing.</p><p>The job is not to prove that the system works. The job is to learn where it fails before someone depends on it.</p><div><hr></div><h1>An example: evaluating a KPI commentary assistant</h1><p>To make this concrete, consider a workflow that many data teams will recognize.</p><p>A GenAI assistant receives a monthly KPI table and writes a short business commentary:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MtHV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3054c7d0-b4d0-4d29-b704-bf6fb227f8c1_1170x119.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MtHV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3054c7d0-b4d0-4d29-b704-bf6fb227f8c1_1170x119.png 424w, https://substackcdn.com/image/fetch/$s_!MtHV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3054c7d0-b4d0-4d29-b704-bf6fb227f8c1_1170x119.png 848w, https://substackcdn.com/image/fetch/$s_!MtHV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3054c7d0-b4d0-4d29-b704-bf6fb227f8c1_1170x119.png 1272w, https://substackcdn.com/image/fetch/$s_!MtHV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3054c7d0-b4d0-4d29-b704-bf6fb227f8c1_1170x119.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MtHV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3054c7d0-b4d0-4d29-b704-bf6fb227f8c1_1170x119.png" width="1170" height="119" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3054c7d0-b4d0-4d29-b704-bf6fb227f8c1_1170x119.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:119,&quot;width&quot;:1170,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:14418,&quot;alt&quot;:&quot;The GenAI Skill Data Professionals Need Most: Evaluation&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/197661414?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3054c7d0-b4d0-4d29-b704-bf6fb227f8c1_1170x119.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The GenAI Skill Data Professionals Need Most: Evaluation" title="The GenAI Skill Data Professionals Need Most: Evaluation" srcset="https://substackcdn.com/image/fetch/$s_!MtHV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3054c7d0-b4d0-4d29-b704-bf6fb227f8c1_1170x119.png 424w, https://substackcdn.com/image/fetch/$s_!MtHV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3054c7d0-b4d0-4d29-b704-bf6fb227f8c1_1170x119.png 848w, https://substackcdn.com/image/fetch/$s_!MtHV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3054c7d0-b4d0-4d29-b704-bf6fb227f8c1_1170x119.png 1272w, https://substackcdn.com/image/fetch/$s_!MtHV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3054c7d0-b4d0-4d29-b704-bf6fb227f8c1_1170x119.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The expected output should:</p><ul><li><p>Identify that revenue declined</p></li><li><p>Mention lower acquisition and higher churn</p></li><li><p>Avoid claiming causality unless supported</p></li><li><p>Keep the commentary concise and business-appropriate</p></li></ul><p>Then you evaluate:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3sBg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbae7e556-8890-492f-9331-64f99be0ea98_1176x307.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3sBg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbae7e556-8890-492f-9331-64f99be0ea98_1176x307.png 424w, https://substackcdn.com/image/fetch/$s_!3sBg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbae7e556-8890-492f-9331-64f99be0ea98_1176x307.png 848w, https://substackcdn.com/image/fetch/$s_!3sBg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbae7e556-8890-492f-9331-64f99be0ea98_1176x307.png 1272w, https://substackcdn.com/image/fetch/$s_!3sBg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbae7e556-8890-492f-9331-64f99be0ea98_1176x307.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3sBg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbae7e556-8890-492f-9331-64f99be0ea98_1176x307.png" width="1176" height="307" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bae7e556-8890-492f-9331-64f99be0ea98_1176x307.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:307,&quot;width&quot;:1176,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:41497,&quot;alt&quot;:&quot;The GenAI Skill Data Professionals Need Most: Evaluation&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/197661414?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbae7e556-8890-492f-9331-64f99be0ea98_1176x307.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The GenAI Skill Data Professionals Need Most: Evaluation" title="The GenAI Skill Data Professionals Need Most: Evaluation" srcset="https://substackcdn.com/image/fetch/$s_!3sBg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbae7e556-8890-492f-9331-64f99be0ea98_1176x307.png 424w, https://substackcdn.com/image/fetch/$s_!3sBg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbae7e556-8890-492f-9331-64f99be0ea98_1176x307.png 848w, https://substackcdn.com/image/fetch/$s_!3sBg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbae7e556-8890-492f-9331-64f99be0ea98_1176x307.png 1272w, https://substackcdn.com/image/fetch/$s_!3sBg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbae7e556-8890-492f-9331-64f99be0ea98_1176x307.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This gives you something concrete to recognize in your own work. It also separates your evaluation from a generic evaluation.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/the-genai-skill-data-professionals?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/the-genai-skill-data-professionals?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><div><hr></div><h1>A simple evaluation loop for data professionals</h1><p>You do not need a full evaluation framework to start.</p><p>A five-step loop is enough:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OqZC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cea89b1-53cb-4a51-ad30-91847d7e79eb_2001x191.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OqZC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cea89b1-53cb-4a51-ad30-91847d7e79eb_2001x191.png 424w, https://substackcdn.com/image/fetch/$s_!OqZC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cea89b1-53cb-4a51-ad30-91847d7e79eb_2001x191.png 848w, https://substackcdn.com/image/fetch/$s_!OqZC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cea89b1-53cb-4a51-ad30-91847d7e79eb_2001x191.png 1272w, https://substackcdn.com/image/fetch/$s_!OqZC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cea89b1-53cb-4a51-ad30-91847d7e79eb_2001x191.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OqZC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cea89b1-53cb-4a51-ad30-91847d7e79eb_2001x191.png" width="1456" height="139" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9cea89b1-53cb-4a51-ad30-91847d7e79eb_2001x191.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:139,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:26854,&quot;alt&quot;:&quot;The GenAI Skill Data Professionals Need Most: Evaluation&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/197661414?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cea89b1-53cb-4a51-ad30-91847d7e79eb_2001x191.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The GenAI Skill Data Professionals Need Most: Evaluation" title="The GenAI Skill Data Professionals Need Most: Evaluation" srcset="https://substackcdn.com/image/fetch/$s_!OqZC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cea89b1-53cb-4a51-ad30-91847d7e79eb_2001x191.png 424w, https://substackcdn.com/image/fetch/$s_!OqZC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cea89b1-53cb-4a51-ad30-91847d7e79eb_2001x191.png 848w, https://substackcdn.com/image/fetch/$s_!OqZC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cea89b1-53cb-4a51-ad30-91847d7e79eb_2001x191.png 1272w, https://substackcdn.com/image/fetch/$s_!OqZC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9cea89b1-53cb-4a51-ad30-91847d7e79eb_2001x191.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p><strong>1. Define the job clearly.</strong> What should the GenAI system actually do?</p><p><strong>2. Collect representative test cases.</strong> Not just the clean ones. Include strange, messy, and borderline cases.</p><p><strong>3. Write a simple scoring rubric.</strong> For structured outputs, use exact-match checks. For subjective quality, use a binary (0/1) rubric or a short LLM-as-a-judge prompt.</p><p><strong>4. Compare outputs across prompts, models, or versions against the same cases.</strong> Eval guidance emphasizes using evals to test and iterate rather than relying on ad hoc inspection.</p><p><strong>5. Record recurring failures.</strong> The failure pattern matters more than one overall score.</p><p>The evaluation stack reflects this general pattern as well: evaluation datasets, rubric-based measures, deterministic metrics where suitable, and custom checks for task-specific requirements.</p><div><hr></div><h1>Why data professionals are well-positioned to own this</h1><p>Evaluation is not strange to data professionals.</p><p>It draws on habits we already use:</p><ul><li><p>Defining success criteria</p></li><li><p>Building representative samples</p></li><li><p>Distinguishing anecdotes from evidence</p></li><li><p>Checking edge cases</p></li><li><p>Analyzing errors instead of celebrating one good result</p></li><li><p>Deciding whether an output is fit for use</p></li></ul><p>Data professionals already have much of the mindset evaluation requires.</p><p>The shift is applying that discipline to generative systems, not only predictive models.</p><p>That is why evaluation may become one of the most valuable GenAI-adjacent skills for analysts, data scientists, analytics engineers, and ML practitioners.</p><div><hr></div><h1>Conclusion</h1><p>Evaluation is what turns GenAI from something impressive into something dependable. For data professionals, that matters more than learning a clever prompt pattern or chasing the newest model release. </p><p>The real advantage comes from knowing how to define good output, test it against realistic cases, trace where it fails, and decide whether it is ready to support actual work. </p><p>That habit is already close to how strong data professionals think: establish the standard, examine the evidence, and avoid trusting a result just because it looks convincing. GenAI simply gives that discipline a new place to matter. As these systems move deeper into analysis, reporting, search, and decision support, the professionals who can evaluate them well will be the ones who make them genuinely useful.</p>]]></content:encoded></item><item><title><![CDATA[The ML Skills That Still Matter in 2026]]></title><description><![CDATA[The skills that still make you desirable by companies and ahead of everyone]]></description><link>https://www.nb-data.com/p/the-ml-skills-that-still-matter-in</link><guid isPermaLink="false">https://www.nb-data.com/p/the-ml-skills-that-still-matter-in</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Thu, 30 Apr 2026 02:01:17 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!xNHD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7481d4bb-f363-449a-bacb-b3f38b585a86_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xNHD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7481d4bb-f363-449a-bacb-b3f38b585a86_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xNHD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7481d4bb-f363-449a-bacb-b3f38b585a86_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!xNHD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7481d4bb-f363-449a-bacb-b3f38b585a86_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!xNHD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7481d4bb-f363-449a-bacb-b3f38b585a86_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!xNHD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7481d4bb-f363-449a-bacb-b3f38b585a86_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xNHD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7481d4bb-f363-449a-bacb-b3f38b585a86_1024x1024.png" width="1024" height="1024" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7481d4bb-f363-449a-bacb-b3f38b585a86_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:855757,&quot;alt&quot;:&quot;The ML Skills That Still Matter in 2026&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/195882313?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7481d4bb-f363-449a-bacb-b3f38b585a86_1024x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The ML Skills That Still Matter in 2026" title="The ML Skills That Still Matter in 2026" srcset="https://substackcdn.com/image/fetch/$s_!xNHD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7481d4bb-f363-449a-bacb-b3f38b585a86_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!xNHD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7481d4bb-f363-449a-bacb-b3f38b585a86_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!xNHD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7481d4bb-f363-449a-bacb-b3f38b585a86_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!xNHD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7481d4bb-f363-449a-bacb-b3f38b585a86_1024x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A lot of data professionals are trying to decide what to learn next.</p><p>That is understandable.</p><p>The field feels crowded now. There is classical machine learning. Deep learning. Generative AI. RAG. Agents. Evaluation. MLOps. Analytics engineering. Data products. Every few months, a new tool or workflow becomes the thing everyone talks about.</p><p>So the question becomes narrow:</p><p><em>Which machine learning skills are still worth learning in 2026?</em></p><p>The answer is not &#8220;learn every model.&#8221;<br>It is also not &#8220;ML is dead because GenAI can do everything.&#8221;</p><p>A better answer is this:</p><p><strong>The ML skills that still matter are the skills that help you turn data into reliable decisions.</strong></p><p>That includes framing the problem, checking the data, building baselines, choosing metrics, validating properly, analyzing errors, and monitoring what happens after deployment.</p><p>These skills matter because AI adoption is no longer experimental. Research shows that generative AI adoption has moved quickly into the mainstream, with reported population adoption reaching 53% within three years. But adoption does not automatically mean reliability. The more AI systems enter daily workflows, the more important it becomes to know whether the output can be trusted.</p><p>That is where machine learning fundamentals still matter.</p><p>By the end of this article, you should have a clearer answer to three questions:</p><ul><li><p>Which ML skills are still worth learning?</p></li><li><p>Why do they still matter in a GenAI-heavy world?</p></li><li><p>What should you actually practice if you want to stay useful as a data professional?</p></li></ul><p>Curious about it? Let&#8217;s get into it.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h1>1. Turn vague business problems into ML problems</h1><p>The first ML skill that still matters is problem framing.</p><p>Most weak ML projects do not fail because the model was not advanced enough. They fail because the problem was unclear from the beginning.</p><p>Someone says:<br>&#8220;Can we predict churn?&#8221;</p><p>That sounds like a machine learning problem. But it is not specific enough yet. Before modeling, you need to define, for example:</p><p><strong>What is the prediction target?</strong> Churn within the next 30 days</p><p><strong>What is the prediction unit?</strong>  One customer account</p><p><strong>When is the prediction made?</strong> Every Monday morning</p><p><strong>What data is available at that time?</strong> Activity, billing, support, and product usage up to Sunday night</p><p><strong>What action will follow the prediction?</strong> Retention team contacts high-risk accounts</p><p><strong>What does success mean?</strong> Lower churn rate, higher retained revenue, better prioritization</p><p>Without these definitions, the model may still produce a score. But the score may not be useful.</p><p>This is the first practical lesson: A machine learning problem is not defined by the model you use. It is defined by the decision you want to improve.</p><p>A good ML practitioner knows how to translate a messy business request into something testable. That means asking whether we are predicting, ranking, classifying, detecting, recommending, or forecasting. You need to identify the exact target variable, the relevant time window, the information available at prediction time, the end-user of the result, and exactly what they will do differently because of the model.</p><p>This skill still matters in 2026 because GenAI tools can help you write code faster, but they do not automatically define the right problem for you. If the target is wrong, the rest of the pipeline is just a waste.</p><div><hr></div><h1>2. Audit the data before trusting the model</h1><p>The second skill is data auditing. This is more than &#8220;clean the data.&#8221;</p><p>Cleaning data usually means handling missing values, fixing formats, removing duplicates, and standardizing columns. Data auditing goes deeper. It asks whether the data is actually valid for the problem. For an ML project, you need to inspect at least five things:</p><p><strong>Label quality: </strong>Is the target variable correct and consistently defined?</p><p><strong>Data availability: </strong>Would these features be available when the prediction is made?</p><p><strong>Leakage: </strong>Does the dataset contain future information?</p><p><strong>Sampling bias: </strong>Does the training data represent the population where the model will be used?</p><p><strong>Stability: </strong>Are the patterns likely to hold over time?</p><p>Data leakage is especially important. Scikit-learn&#8217;s documentation warns that leakage can produce overly optimistic Performance because information from the test set or future data accidentally enters the training process.</p><p>A simple example:</p><p>You are building a churn model. Your dataset includes a column called cancellation_reason. The model performs extremely well.</p><p>But there is a problem. That column only exists after the customer has already churned. The model is not learning early churn signals. It is reading the answer key.</p><p>This happens more often than people admit. It can appear in many forms:</p><p>For example, in <strong>Credit risk</strong>, it uses a manual review result that happens after application submission or in <strong>Fraud detection</strong>, it uses an investigation outcome as an input feature.</p><p>This is why data auditing remains a core ML skill. A data professional should be able to look at a dataset and ask: &#8220;Would I know this information at the moment I need to make the prediction?&#8221; If the answer is no, the feature probably should not be there.</p><div><hr></div><h1>3. Build simple baselines before complex models</h1><p>The third skill is baseline building. A baseline is the simplest model or rule that gives you a reference point. It could be a majority-class classifier, a simple business rule, logistic regression, a decision tree, a moving average, a keyword-based classifier, or a simple ranking score.</p><p>Baselines are not boring. They are protection against unnecessary complexity.</p><p>Google&#8217;s Rules of Machine Learning still emphasize starting with simple models and robust infrastructure before moving into more complex ML systems. That advice has aged well because the easiest mistake in modern AI work is to overbuild too early.</p><p>Suppose you are building a lead scoring model. A simple baseline might be: Score leads higher if they visited the pricing page, opened two emails, and work at a company above a certain size.</p><p>Then you compare that with a logistic regression model. Then maybe a gradient boosting model. Only after that should you consider something more complex.</p><p>The practical question is not: &#8220;Can I use a more advanced model?&#8221;</p><p>The better question is: &#8220;Does the more advanced model produce enough improvement to justify the added complexity?&#8221;</p><p>That complexity includes harder debugging, higher maintenance costs, more difficult explanations, more monitoring requirements, more fragile deployments, and more stakeholder confusion.</p><p>In many real business settings, the best model is not the most advanced model. It is the simplest model that performs well enough and can be trusted by the people who need to use it.</p><div><hr></div><h1>4. Choose metrics based on the cost of mistakes</h1><p>The fourth skill is metric selection. This is where many ML projects become misleading. People often ask: &#8220;What is the model accuracy?&#8221;</p><p>But accuracy is not always the right metric. For example, imagine a fraud detection dataset where only 1% of transactions are fraudulent. A model that predicts &#8220;not fraud&#8221; for everything can be 99% accurate.</p><p>That sounds excellent. It is also useless.</p><p>Metric choice depends on the type of mistake you care about.</p><p>For instance, <strong>in Fraud detection</strong>, you care about recall, precision, and false positive rate. In Medical screening, recall and false negatives are critical. In Spam detection, it is precision and false positives. <strong>In Credit risk</strong>, you look at calibration, precision, recall, and expected loss. <strong>In Recommendation systems</strong>, ranking metrics, conversion, and retention matter most.</p><p>This is one of the most useful ML skills for real work: You need to connect the metric to the decision.</p><p>If the business can only contact 500 customers per week, then overall accuracy may not matter much. What matters is whether the top 500 predicted customers are actually worth contacting.</p><p>In that case, you may care about precision at K, lift in the top decile, expected retained revenue, conversion from intervention, or the cost per saved customer.</p><p>The right metric depends on the operating reality. This is why ML is not only a technical exercise. A model is evaluated inside a business process. If the metric ignores that process, the evaluation is incomplete.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/the-ml-skills-that-still-matter-in?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/the-ml-skills-that-still-matter-in?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><div><hr></div><h1>5. Validate without fooling yourself</h1><p>The fifth skill is validation. This is where you test whether the model can generalize.</p><p>A model can perform well on the data it has seen. That does not mean it will perform well on new data. This is why train/test splits, cross-validation, time-based validation, and holdout sets still matter.</p><p>Scikit-learn&#8217;s cross-validation documentation highlights the importance of evaluating estimator performance properly and avoiding pitfalls that make performance estimates unreliable.</p><p>The important idea is simple: Your validation setup should imitate the real situation where the model will be used.</p><p>For random customer classification, a random train/test split might be fine. For time-dependent problems, it may not be. If you are forecasting demand, predicting churn, detecting fraud, or scoring leads over time, you often need time-based validation. That means training on the past and testing on the future.</p><p>For example, you might train on data from January to March to validate in April. Then train from January to April to validate in May, and train from January to May to validate in June.</p><p>This is closer to the real world. You do not get to train in June to predict April. Validation should respect time.</p><p>A strong data professional knows how to ask:</p><ul><li><p>Was the test set kept separate?</p></li><li><p>Was the preprocessing fitted only on the training data?</p></li><li><p>Was the split random when it should have been time-based?</p></li><li><p>Were duplicate users or records split across train and test?</p></li><li><p>Was the test set reused too many times during model selection?</p></li><li><p>Does Performance hold across segments?</p></li></ul><p>This skill matters because bad validation creates false confidence. And false confidence is dangerous.</p><div><hr></div><h1>6. Analyze errors, not just scores</h1><p>The sixth skill is error analysis. A single score is not enough.</p><p>A model with 88% accuracy may still fail badly for the most important cases. A forecasting model with a good average error may still miss peak demand. A churn model may perform well overall but fail for enterprise customers. A document classifier may work for clean English documents but fail for short, messy, multilingual text.</p><p>This is why error analysis matters. After you evaluate the model, you should inspect where it fails.</p><p>A practical error analysis table might look like this:</p><p><strong>New users</strong>: Does the model fail when history is limited?</p><p><strong>High-value customers</strong>: Does it work for the accounts that matter most?</p><p><strong>Geography</strong>: Does Performance differ by country or region?</p><p><strong>Product category</strong>: Does it fail on long-tail categories?</p><p><strong>Time period</strong>: Does it degrade during holidays or campaigns?</p><p><strong>Input quality</strong>: Does messy or incomplete data hurt Performance?</p><p><strong>Minority class</strong>: Does the model ignore rare but important cases?</p><p>This is where ML work becomes diagnostic. You stop asking only: &#8220;Is the model good?&#8221; You start asking: &#8220;Where is the model useful, where is it weak, and where should we not trust it?&#8221;</p><p>That is a much better standard. This skill also connects directly to GenAI evaluation. When evaluating an LLM workflow, the same habit applies. You do not only ask whether the average output looks good. You ask which prompts fail, which user intents fail, which document types fail, which languages fail, which edge cases create hallucination, and which outputs need human review.</p><p>The models changed, but the evaluation habit still transfers.</p><div><hr></div><h1>7. Understand Monitoring after deployment</h1><p>The seventh skill is model monitoring. A model is not finished when it is deployed. It enters a changing environment.</p><p>Customer behavior changes. Product features change. Fraud patterns change. Marketing channels change. Economic conditions change. Data pipelines change. Even column definitions can change.</p><p>Google Cloud&#8217;s model monitoring documentation discusses feature skew and drift detection for deployed models, including changes in categorical and numerical input features.</p><p>For most data professionals, the key idea is not complicated: A model can become worse even if the code does not change.</p><p>That means you need to monitor:</p><p><strong>Data freshness: </strong>Did the latest batch arrive?</p><p><strong>Input distribution: </strong>Are feature values changing?</p><p><strong>Prediction distribution: </strong>Are model scores suddenly higher or lower?</p><p><strong>Segment performance: </strong>Is one customer group degrading faster?</p><p><strong>Business outcome: </strong>Is the model still improving the intended metric?</p><p><strong>Pipeline health: </strong>Are transformations still working correctly?</p><p><strong>Human feedback: </strong>Are users overriding or ignoring the model?</p><p>This is one reason ML skills remain useful in 2026. Many AI systems fail quietly. They do not always break in obvious ways. They drift. They degrade. They become misaligned with the current workflow. Monitoring is how you notice before the damage becomes expensive.</p><div><hr></div><h1>8. Explain the model in business terms</h1><p>The eighth skill is communication. But not generic communication. The useful skill is being able to explain the model in terms of decisions, trade-offs, and risk.</p><p>A stakeholder does not only need to know that the model has 0.82 AUC. They need to know what that means.</p><p>For example: &#8220;The model is useful for ranking customers by churn risk, but it should not be used as an automatic cancellation prediction. It works better for customers with at least three months of activity history. For new customers, the signal is weaker.&#8221;</p><p>That explanation is much more useful than a metric alone. A good ML explanation should include:</p><p>A good ML explanation should include the Purpose (what decision the model supports), the Scope (where it should and should not be used), the Performance (how well it works and compared to what), Failure modes (where it performs poorly), Trade-offs (what happens if we optimize for precision vs recall), Action (what users should do with the output), and Monitoring (what needs to be checked after deployment).</p><p>This is especially important as AI systems become more embedded in business workflows. NIST&#8217;s AI Risk Management Framework emphasizes test, evaluation, verification, and validation across the AI lifecycle. That kind of thinking is not only for regulators or governance teams. It is also practical for data professionals who need to explain when a model is reliable enough to use.</p><p>The best ML people are not only model builders. They are translators between model behavior and business action.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/the-ml-skills-that-still-matter-in/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/the-ml-skills-that-still-matter-in/comments"><span>Leave a comment</span></a></p><div><hr></div><h1>So what should you learn in 2026?</h1><p>If you are learning ML now, do not structure your learning around a long list of algorithms. Structure it around the workflow. Here is a better learning path.</p><p><strong>Problem framing</strong><em> | Prediction target, decision point, action, success metric | Practice: </em>A one-page ML problem definition</p><p><strong>Data audit</strong><em> | Leakage, label quality, missingness, sampling, availability | Practice: </em>A data quality checklist</p><p><strong>Baseline modeling</strong><em> | Rules, logistic regression, trees, simple benchmarks | Practice: </em>A baseline comparison table</p><p><strong>Metric selection</strong><em> | Precision, recall, F1, AUC, calibration, ranking metrics | Practice: </em>A metric justification note</p><p><strong>Validation</strong><em> | Train/test split, cross-validation, time-based split | Practice: </em>A validation design</p><p><strong>Error analysis</strong><em> | Segment performance, false positives, false negatives | Practice: </em>An error analysis report</p><p><strong>Monitoring</strong><em> | Drift, skew, data freshness, prediction distribution | Practice: </em>A monitoring checklist</p><p><strong>Communication</strong><em> | Trade-offs, limitations, recommended use | Practice: </em>A stakeholder summary</p><p>This is the part many courses skip. They teach the model before the workflow. </p><p>But in real work, the workflow is what makes the model useful.</p><h1>The skills that still matter</h1><p>So, what ML skills still matter in 2026? These ones:</p><ul><li><p>Framing a vague business problem into a clear ML task</p></li><li><p>Auditing data before trusting it</p></li><li><p>Detecting leakage and bad labels</p></li><li><p>Building simple baselines</p></li><li><p>Choosing metrics based on business cost</p></li><li><p>Validating models correctly</p></li><li><p>Analyzing errors by segment</p></li><li><p>Monitoring model behavior after deployment</p></li><li><p>Explaining limitations clearly</p></li></ul><p>These skills are not trendy. But they are durable. They matter for classical ML. They matter for GenAI evaluation. They matter for RAG systems. They matter for AI products.</p><p>They matter whenever someone asks: &#8220;Can we trust this output enough to use it?&#8221;</p><p>That is the real reason ML still matters. Not because every data professional needs to train models from scratch. Not because classical ML is competing with GenAI. But because ML teaches the discipline behind reliable AI work.</p><p>In 2026, the most valuable data professionals will not be the ones who chase every new tool. They will be the ones who can build, evaluate, question, and explain AI systems clearly.</p><p>That is still machine learning.</p><p>And it still matters.</p><div><hr></div>]]></content:encoded></item><item><title><![CDATA[What Real SQL Work Taught Me About Being a Data Scientist]]></title><description><![CDATA[Why I stopped seeing SQL as a secondary skill and started seeing it as the backbone of real data projects]]></description><link>https://www.nb-data.com/p/what-real-sql-work-taught-me-about</link><guid isPermaLink="false">https://www.nb-data.com/p/what-real-sql-work-taught-me-about</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Sat, 28 Mar 2026 15:07:48 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!7CmX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4abfb659-bac8-4b53-b22e-24fcad7b32ea_1312x736.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7CmX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4abfb659-bac8-4b53-b22e-24fcad7b32ea_1312x736.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7CmX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4abfb659-bac8-4b53-b22e-24fcad7b32ea_1312x736.jpeg 424w, https://substackcdn.com/image/fetch/$s_!7CmX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4abfb659-bac8-4b53-b22e-24fcad7b32ea_1312x736.jpeg 848w, https://substackcdn.com/image/fetch/$s_!7CmX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4abfb659-bac8-4b53-b22e-24fcad7b32ea_1312x736.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!7CmX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4abfb659-bac8-4b53-b22e-24fcad7b32ea_1312x736.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7CmX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4abfb659-bac8-4b53-b22e-24fcad7b32ea_1312x736.jpeg" width="1312" height="736" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4abfb659-bac8-4b53-b22e-24fcad7b32ea_1312x736.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:736,&quot;width&quot;:1312,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:90217,&quot;alt&quot;:&quot;What Real SQL Work Taught Me About Being a Data Scientist&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/192418111?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4abfb659-bac8-4b53-b22e-24fcad7b32ea_1312x736.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="What Real SQL Work Taught Me About Being a Data Scientist" title="What Real SQL Work Taught Me About Being a Data Scientist" srcset="https://substackcdn.com/image/fetch/$s_!7CmX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4abfb659-bac8-4b53-b22e-24fcad7b32ea_1312x736.jpeg 424w, https://substackcdn.com/image/fetch/$s_!7CmX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4abfb659-bac8-4b53-b22e-24fcad7b32ea_1312x736.jpeg 848w, https://substackcdn.com/image/fetch/$s_!7CmX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4abfb659-bac8-4b53-b22e-24fcad7b32ea_1312x736.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!7CmX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4abfb659-bac8-4b53-b22e-24fcad7b32ea_1312x736.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Image by Ideogram.ai</figcaption></figure></div><div class="pullquote"><p>"Real SQL work taught me that trustworthy definitions matter more than flashy queries."</p></div><h1>I did not start by taking SQL seriously</h1><p style="text-align: justify;">Early in my career, I did not see SQL as central to being a data scientist. Most of my learning was built around Python, and the classes and bootcamps I joined reinforced that view. Python felt like the real language of data science. SQL felt useful, but distant.</p><p>So I did not reject it. I simply did not get enough exposure to it.</p><p>That distinction matters. When your early learning path is dominated by notebooks, models, and Python libraries, it is easy to assume that the real work starts once the data is already in front of you. In that worldview, SQL looks like preparation work. Helpful, yes. Foundational, no.</p><p>Real work changed that view gradually.</p><p>The more I worked in corporate settings, the clearer it became that many projects do not begin with modeling, dashboards, or machine learning. They begin with a more basic set of questions: Is the data available? Is the definition correct? Can the result be trusted enough for someone to act on it?</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h1>Work forced the lesson</h1><p>What changed my view of SQL was not one dramatic moment. It was the accumulation of projects. Again and again, the work pulled me toward the same reality: before anything becomes an analysis, a model, or a recommendation, someone has to make sure the data is available, correctly defined, and usable.</p><p>That is where SQL kept appearing.</p><p>Sometimes the request looked simple. A business team needed a report. Sometimes the request sounded more strategic. A project needed insight to inform a decision. Sometimes the work moved beyond a single analysis into the project's production life. In each case, SQL mattered not only for retrieving data, but for deciding whether the project itself rested on a solid foundation.</p><p>The difficult conversations were often not about syntax at all. They were about meaning. What exactly should count as a sale? Which time window should be used? Which source should be treated as the source of truth? If two tables produce different answers, which one reflects the real business process?</p><p>That was the point where SQL stopped feeling like a supporting skill and became infrastructure.</p><h1>What real SQL work actually looked like</h1><p>The lesson became clearer through a few recurring types of work. These were not glamorous, as they were simply the places where SQL kept proving its value.</p><ul><li><p>Ad-hoc reporting and insight requests that looked simple but hid messy logic and scattered data.</p></li><li><p>Metric definition work, where the challenge was deciding what should count before writing the query.</p></li><li><p>Combining multiple data sources without destroying the business meaning of the result.</p></li><li><p>Preparing the right data for downstream analysis and modeling in Python.</p></li></ul><div><hr></div><h2>1. Ad-hoc reporting taught me that simple requests are rarely simple</h2><p>A lot of real SQL work starts with a seemingly harmless request. The business needs a report. Someone wants a quick performance update. A team asks for insight before a meeting. On paper, it sounds like a straightforward query.</p><p>In practice, it rarely is.</p><p>Sometimes the data is not available in one place. Sometimes it lives across several sources that were never designed to fit together neatly. Sometimes the logic needed to answer the question is more complicated than the request suggests. And often the timeline is short, so you do not have the luxury of slowly wading through the data.</p><p>That changed how I think about SQL skills. In real reporting work, the challenge is not just writing something that runs. The challenge is moving from a vague business question to a reliable answer under real constraints. That takes judgment, prioritization, and a clear sense of what the output needs to mean.</p><p>Useful SQL work is often less glamorous than people expect. It is not always about elegant tricks. Very often, it is about getting the right answer quickly enough to matter, without breaking the logic behind it.</p><div><hr></div><h2>2. Metric definition matters more than query complexity</h2><p>If there is one area where real SQL work changed me the most, it is the definition of metrics.</p><p>In theory, a metric looks clean. In practice, even something as familiar as a sales number can go wrong depending on the time scope, exclusions, business rules, and source tables. A number can look precise and still be misleading if two teams are working from different assumptions or if one table captures the event differently from another.</p><p>That is why some SQL problems cannot be solved by clever syntax alone. You can write a technically correct query and still produce the wrong business answer.</p><p>The real work is often more basic and more demanding at the same time:</p><ul><li><p>deciding what should count</p></li><li><p>deciding what should be excluded</p></li><li><p>choosing which table reflects the operational truth</p></li><li><p>making sure the result matches the way the business actually works</p></li></ul><p>This is where collaboration becomes essential. There are many situations where the data exists, but understanding it requires discussion with business users who know the process behind the records. Without that alignment, a query may return rows but not the truth.</p><p>Over time, I started to see that some of the most dangerous problems in data work are not computational. They are definitional. A wrong definition can quietly damage a project, mislead stakeholders, or erode trust in the team long before anyone notices the issue.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/what-real-sql-work-taught-me-about?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/what-real-sql-work-taught-me-about?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><div><hr></div><h2>3. Combining data sources is harder than it looks</h2><p>Another lesson real SQL work taught me is that combining information from multiple sources without losing meaning is much harder than it first appears.</p><p>From the outside, joins can look like a purely technical step. In practice, they can become one of the most delicate parts of a project. Sometimes a clean primary key does not exist. Sometimes the relationship is not direct. Sometimes aggregation is needed before two datasets can even be compared. And sometimes each source reflects a slightly different view of the same business concept.</p><p>That creates several risks at once: duplicate rows, dropped records, timing mismatches, and numbers that appear structurally valid but are conceptually incorrect.</p><p>This is why SQL work often requires more collaboration than people expect. To combine sources responsibly, you frequently need validation from multiple stakeholders. The challenge is not merely to make the query run. The challenge is to preserve validity.</p><p>For me, this was one of the clearest moments where SQL became inseparable from business understanding. Good SQL was not just about retrieval. It was about preserving meaning as it moved across systems.</p><div><hr></div><h2>4. Even Python-heavy data science often begins with SQL</h2><p>Because my early learning path emphasized Python, I initially imagined that most serious data-science work would begin there. In reality, SQL was often necessary before I could even start proper work in Python.</p><p>If the data lived in a SQL database, then SQL was the gatekeeper. It was how I extracted the relevant population, selected the appropriate time window, assembled the required columns, and checked whether the data were suitable for the task ahead. Whether the next step was exploratory analysis, feature preparation, modeling, or evaluation, SQL was often the first step.</p><p>That changed how I think about the relationship between SQL and data science. SQL is not simply what happens before the interesting work. Very often, it is part of the interesting work.</p><p>If the population is wrong, the feature set is incomplete, or the definition is unstable, the downstream Python work inherits that weakness. In that sense, SQL does not sit beneath data science. It sits inside it.</p><div><hr></div><h1>What I value in SQL work now</h1><p>Real work also changed how I evaluate SQL skills in others and in myself.</p><p>I still care about writing cleaner, more efficient queries, especially as data grows larger and execution speed matters. But that is no longer the first thing I look for.</p><p>What I value first is this:</p><blockquote><p>1. Correctness. The wrong data can quietly damage an entire project.</p><p>2. Stakeholder trust. Data work only becomes valuable when other people believe the result is dependable.</p><p>3. Maintainability. Many projects do not end after a single request, so someone has to live with the logic later.</p></blockquote><p>A strong SQL practitioner, in my view, is not simply someone who knows a large amount of syntax. It is someone who understands the data definition, knows how to acquire the data in the most reliable way, and can produce logic that remains useful beyond the moment it was written.</p><div><hr></div><h1>What I would tell aspiring data scientists now</h1><p>If your learning path has focused mostly on Python, I would say this clearly: do not treat SQL as optional.</p><p>You do not need to memorize every feature of the language before doing meaningful work. Documentation exists, and syntax can be learned as needed. But you do need to understand why SQL matters. It matters because data projects depend on access to the right data, under the right definitions, with logic that can withstand real business use.</p><p>That is the part I wish I had understood earlier. SQL is not important because it looks technical. It is important because it sits close to the truth conditions of data work. It is where data availability is tested. It is where definitions get challenged. It is where numbers either become trustworthy or fall apart.</p><p>For me, that has become one of the clearest professional lessons of real data work. SQL is not the opposite of data science, nor is it a lower-level skill beneath it. In many organizations, SQL is one of the foundations that allows data science to be useful at all.</p><p>And if there is one line I would leave readers with, it is this: real SQL work taught me that trustworthy definitions matter more than flashy queries.</p><p>If you are learning SQL now, learn it through real use cases. Learn it through reporting, metric definition, source validation, and the kind of business questions that force you to care about correctness. </p><div><hr></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/what-real-sql-work-taught-me-about/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/what-real-sql-work-taught-me-about/comments"><span>Leave a comment</span></a></p>]]></content:encoded></item><item><title><![CDATA[Best Stock Market data API in the AI Agent era]]></title><description><![CDATA[How modern financial APIs are powering the next generation of AI-driven market research and trading tools]]></description><link>https://www.nb-data.com/p/best-stock-market-data-api-in-the</link><guid isPermaLink="false">https://www.nb-data.com/p/best-stock-market-data-api-in-the</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Sat, 14 Mar 2026 07:49:27 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1560221328-12fe60f83ab8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxzdG9ja3xlbnwwfHx8fDE3NzMxNDkwODZ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1560221328-12fe60f83ab8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxzdG9ja3xlbnwwfHx8fDE3NzMxNDkwODZ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1560221328-12fe60f83ab8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxzdG9ja3xlbnwwfHx8fDE3NzMxNDkwODZ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1560221328-12fe60f83ab8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxzdG9ja3xlbnwwfHx8fDE3NzMxNDkwODZ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1560221328-12fe60f83ab8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxzdG9ja3xlbnwwfHx8fDE3NzMxNDkwODZ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1560221328-12fe60f83ab8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxzdG9ja3xlbnwwfHx8fDE3NzMxNDkwODZ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1560221328-12fe60f83ab8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxzdG9ja3xlbnwwfHx8fDE3NzMxNDkwODZ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="4800" height="3188" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1560221328-12fe60f83ab8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxzdG9ja3xlbnwwfHx8fDE3NzMxNDkwODZ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:3188,&quot;width&quot;:4800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;close-up photo of monitor displaying graph&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="close-up photo of monitor displaying graph" title="close-up photo of monitor displaying graph" srcset="https://images.unsplash.com/photo-1560221328-12fe60f83ab8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxzdG9ja3xlbnwwfHx8fDE3NzMxNDkwODZ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1560221328-12fe60f83ab8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxzdG9ja3xlbnwwfHx8fDE3NzMxNDkwODZ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1560221328-12fe60f83ab8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxzdG9ja3xlbnwwfHx8fDE3NzMxNDkwODZ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1560221328-12fe60f83ab8?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwyfHxzdG9ja3xlbnwwfHx8fDE3NzMxNDkwODZ8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@bash__profile">Nicholas Cappello</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>The stock market data API landscape is changing. In the past, developers mostly evaluated providers on familiar dimensions: coverage, latency, pricing, documentation, and reliability. Those criteria still matter, but the rise of LLM-powered copilots, autonomous research workflows, and multi-agent financial systems has introduced a new requirement: how easily can a data provider plug into agentic software?</p><p>In that environment, the strongest providers are not just the ones with broad datasets. They are the ones that expose clean, structured interfaces that AI agents can query, reason over, and combine with downstream tools for analysis, monitoring, and decision support. Some vendors are already leaning into this shift with MCP servers and LLM-oriented resources. Others remain stronger as enterprise data backbones than as explicitly AI-native platforms.</p><p>In this article, we will explore the Best Stock Market data API in the AI Agent era. Curious about it? </p><p>Let&#8217;s get into it.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h1><strong>Alpha Vantage</strong></h1><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xh-Y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8db652-ca9d-40e1-be34-c06229ff5dc2_2474x1278.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xh-Y!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8db652-ca9d-40e1-be34-c06229ff5dc2_2474x1278.png 424w, https://substackcdn.com/image/fetch/$s_!xh-Y!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8db652-ca9d-40e1-be34-c06229ff5dc2_2474x1278.png 848w, https://substackcdn.com/image/fetch/$s_!xh-Y!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8db652-ca9d-40e1-be34-c06229ff5dc2_2474x1278.png 1272w, https://substackcdn.com/image/fetch/$s_!xh-Y!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8db652-ca9d-40e1-be34-c06229ff5dc2_2474x1278.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xh-Y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8db652-ca9d-40e1-be34-c06229ff5dc2_2474x1278.png" width="1456" height="752" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8d8db652-ca9d-40e1-be34-c06229ff5dc2_2474x1278.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:752,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:182857,&quot;alt&quot;:&quot;Best Stock Market data API in the AI Agent era&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/190589167?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8db652-ca9d-40e1-be34-c06229ff5dc2_2474x1278.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Best Stock Market data API in the AI Agent era" title="Best Stock Market data API in the AI Agent era" srcset="https://substackcdn.com/image/fetch/$s_!xh-Y!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8db652-ca9d-40e1-be34-c06229ff5dc2_2474x1278.png 424w, https://substackcdn.com/image/fetch/$s_!xh-Y!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8db652-ca9d-40e1-be34-c06229ff5dc2_2474x1278.png 848w, https://substackcdn.com/image/fetch/$s_!xh-Y!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8db652-ca9d-40e1-be34-c06229ff5dc2_2474x1278.png 1272w, https://substackcdn.com/image/fetch/$s_!xh-Y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d8db652-ca9d-40e1-be34-c06229ff5dc2_2474x1278.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>Overview</strong></h3><p><a href="https://www.alphavantage.co/">Alpha Vantage</a> is a widely used financial data platform that provides real-time and historical market data APIs for equities, options, forex, cryptocurrencies, and macroeconomic indicators. The platform is designed to support both individual developers and professional trading systems through a simple, developer-friendly interface and a large catalog of market datasets.<br><br>A distinguishing feature of Alpha Vantage is the breadth of its data coverage. The platform delivers real-time and historical financial market data through programmatic APIs and spreadsheet integrations, enabling developers to build trading dashboards, quantitative research pipelines, and automated trading tools on top of a unified data interface.<br><br>The API also provides a rich library of built-in analytics&#8212;including technical indicators and fundamental datasets&#8212;allowing users to retrieve both raw market data and higher-level financial signals without implementing complex calculations themselves. In practice, this makes Alpha Vantage a flexible backbone for applications ranging from educational projects and fintech prototypes to production trading systems and investment research platforms.</p><h3><strong>What makes it valuable in the AI Agent era?</strong></h3><p>Alpha Vantage has become particularly relevant in the emerging ecosystem of LLM-powered financial tools and autonomous AI agents, largely because it provides structured market data in formats that are easy for agents and models to access, reason over, and integrate into automated workflows.<br><br><strong>1. Native integration with AI agent ecosystems via MCP</strong><br>Alpha Vantage provides an official Model Context Protocol (MCP) server, enabling large language models and agent-based applications to directly access financial data through standardized tools. The MCP server allows AI assistants and development environments to query real-time and historical stock market data programmatically, turning the API into a plug-and-play data source for agentic systems. <br><br><strong>2. Compatibility with multi-agent financial research systems</strong><br>Modern agentic trading frameworks increasingly rely on structured financial APIs like Alpha Vantage as data sources. For example, the open-source <a href="https://trading-agents.ai/">TradingAgents framework</a> simulates a professional trading firm using multiple LLM-powered agents&#8212;such as fundamental analysts, technical analysts, sentiment analysts, traders, and risk managers&#8212;that collaborate to analyze equities and make decisions. This system is powered by Alpha Vantage API as the core data backbone. <br><br><strong>3. Documentation and developer assets optimized for machine consumption</strong><br>Another advantage in the LLM era is the structure and accessibility of Alpha Vantage&#8217;s developer resources. The platform provides comprehensive API documentation, examples, and community libraries across many programming languages, making it straightforward for both humans and AI coding agents to integrate financial data pipelines. Because LLM-powered development tools rely heavily on structured documentation, well-defined API endpoints, and example code, this ecosystem of docs, SDKs, and README files makes Alpha Vantage particularly easy for AI systems to learn and use.</p><h3><strong>In short</strong></h3><p>Alpha Vantage&#8217;s combination of structured financial APIs, an MCP interface for AI agents, and extensive developer documentation positions it as a data infrastructure layer for the emerging generation of AI-powered trading tools, research agents, and autonomous financial analysis systems.</p><div><hr></div><h2>Tradier</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5L68!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f43f50f-00df-4007-8f4e-b28403ddf3a0_2399x1459.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5L68!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f43f50f-00df-4007-8f4e-b28403ddf3a0_2399x1459.png 424w, https://substackcdn.com/image/fetch/$s_!5L68!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f43f50f-00df-4007-8f4e-b28403ddf3a0_2399x1459.png 848w, https://substackcdn.com/image/fetch/$s_!5L68!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f43f50f-00df-4007-8f4e-b28403ddf3a0_2399x1459.png 1272w, https://substackcdn.com/image/fetch/$s_!5L68!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f43f50f-00df-4007-8f4e-b28403ddf3a0_2399x1459.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5L68!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f43f50f-00df-4007-8f4e-b28403ddf3a0_2399x1459.png" width="1456" height="885" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5f43f50f-00df-4007-8f4e-b28403ddf3a0_2399x1459.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:885,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:848183,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/190589167?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f43f50f-00df-4007-8f4e-b28403ddf3a0_2399x1459.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5L68!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f43f50f-00df-4007-8f4e-b28403ddf3a0_2399x1459.png 424w, https://substackcdn.com/image/fetch/$s_!5L68!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f43f50f-00df-4007-8f4e-b28403ddf3a0_2399x1459.png 848w, https://substackcdn.com/image/fetch/$s_!5L68!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f43f50f-00df-4007-8f4e-b28403ddf3a0_2399x1459.png 1272w, https://substackcdn.com/image/fetch/$s_!5L68!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f43f50f-00df-4007-8f4e-b28403ddf3a0_2399x1459.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Overview</h3><p><a href="https://tradier.com/">Tradier</a> is a brokerage-focused API platform that combines market data, account access, and trading functionality. Its public API supports real-time, delayed, and historical market data through both request/response endpoints and streaming interfaces, while also exposing brokerage capabilities such as account information, positions, orders, watchlists, and trade execution.</p><p>A key differentiator is that Tradier is not just a data API. It is part of a brokerage stack. That means developers can use it not only to retrieve quotes, options chains, time-and-sales data, and historical pricing, but also to connect agentic workflows directly to trading and portfolio actions. Tradier also supports HTTP and WebSocket streaming, which is useful when building systems that need fast updates rather than purely batch-style analysis.</p><p>Tradier&#8217;s market-data positioning is more U.S.-brokerage-centric than broad all-asset-class research platforms. Real-time data is available to Tradier Brokerage account holders for U.S. stocks and options, and delayed data follows the standard 15-minute model for non-real-time access. That makes Tradier particularly compelling for execution-oriented applications rather than for the widest possible global dataset footprint.</p><h3>What makes it valuable in the AI Agent era?</h3><p><strong>1. MCP and LLM-oriented documentation</strong><br>Tradier is unusually forward-leaning in how it presents its docs to the LLM era. Its documentation includes <code>llms.txt</code>, dedicated LLM resources, and a Tradier MCP section. Tradier&#8217;s own MCP documentation says users can access market data, account details, documentation, and even place trades from within connected AI tools. That makes Tradier one of the few providers publicly bridging financial APIs and conversational interfaces in a first-class way.</p><p><strong>2. Strong fit for execution-capable agents</strong><br>Many financial APIs stop at data retrieval. Tradier goes further by combining data access with brokerage actions such as order placement, account history, positions, and balances. In the AI agent era, that matters because the most interesting systems are often not just research agents but action-taking agents. Tradier is therefore especially relevant for developers building guarded execution workflows, trading copilots, or semi-autonomous assistants that need both read and act capabilities.</p><p><strong>3. Streaming interfaces for real-time agent loops</strong><br>Tradier supports both HTTP and WebSocket streaming for market and account data. That is important for agent architectures that continuously monitor events, react to intraday changes, or trigger downstream workflows when market conditions shift. In practical terms, Tradier is better suited than batch-only APIs for event-driven agents that need live context rather than periodic polling alone.</p><h3>In short</h3><p>Tradier is one of the strongest options for AI agents that need to move beyond analysis into brokerage-connected workflows. It may not be the broadest general-purpose research API, but for U.S.-market, execution-aware agents, Tradier&#8217;s mix of market data, account endpoints, streaming support, and MCP/LLM resources makes it highly relevant.</p><div><hr></div><h2>Xignite</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mBY8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcbb256b-df66-44d4-909a-a9faf81debb0_2777x1331.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mBY8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcbb256b-df66-44d4-909a-a9faf81debb0_2777x1331.png 424w, https://substackcdn.com/image/fetch/$s_!mBY8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcbb256b-df66-44d4-909a-a9faf81debb0_2777x1331.png 848w, https://substackcdn.com/image/fetch/$s_!mBY8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcbb256b-df66-44d4-909a-a9faf81debb0_2777x1331.png 1272w, https://substackcdn.com/image/fetch/$s_!mBY8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcbb256b-df66-44d4-909a-a9faf81debb0_2777x1331.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mBY8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcbb256b-df66-44d4-909a-a9faf81debb0_2777x1331.png" width="1456" height="698" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bcbb256b-df66-44d4-909a-a9faf81debb0_2777x1331.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:698,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2318388,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/190589167?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcbb256b-df66-44d4-909a-a9faf81debb0_2777x1331.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mBY8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcbb256b-df66-44d4-909a-a9faf81debb0_2777x1331.png 424w, https://substackcdn.com/image/fetch/$s_!mBY8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcbb256b-df66-44d4-909a-a9faf81debb0_2777x1331.png 848w, https://substackcdn.com/image/fetch/$s_!mBY8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcbb256b-df66-44d4-909a-a9faf81debb0_2777x1331.png 1272w, https://substackcdn.com/image/fetch/$s_!mBY8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbcbb256b-df66-44d4-909a-a9faf81debb0_2777x1331.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Overview</h3><p><a href="https://www.xignite.com/">Xignite</a> is an enterprise financial data platform centered on cloud-delivered APIs and market-data management. Its catalog covers stock quotes, ETFs and mutual funds, foreign exchange, futures and options, indices and benchmarks, fixed income and rates, company fundamentals, reference data, earnings, and news. The company also emphasizes broad upstream sourcing, stating that its data comes from more than 250 providers, alongside curated in-house datasets.</p><p>Xignite&#8217;s public positioning is less &#8220;developer hobbyist API&#8221; and more enterprise-grade market data infrastructure. It highlights unlimited-usage pricing, flexible commercial packaging by asset class, call frequency, and region, and delivery models that include real-time, historical, and reference data. Its developer materials also show a broad set of products for delayed quotes, real-time quotes, historical data, streaming, alerts, IPOs, and company information.</p><p>That means Xignite is best understood as a data platform for institutions and mature fintech products rather than as a lightweight API-first experimentation layer. For many teams, that is a feature, not a drawback. In an AI stack, the most valuable data provider is often the one that can reliably serve as the normalized source behind internal models, orchestration layers, and production analytics systems. This last point is an inference from Xignite&#8217;s product positioning toward scalable enterprise delivery and market-data management.</p><h3>What makes it valuable in the AI Agent era?</h3><p><strong>1. Enterprise-grade breadth for multi-source agent pipelines</strong><br>AI agents become more useful when they can combine quotes, fundamentals, benchmarks, reference data, and news into a single reasoning loop. Xignite&#8217;s catalog is strong on this dimension. Because it covers a wide range of asset classes and reference datasets, it can act as the structured data layer beneath enterprise financial copilots and internal analyst tools.</p><p><strong>2. Strong fit for organizations building their own orchestration layer</strong><br>Unlike Alpha Vantage or EODHD, Xignite&#8217;s public materials emphasize APIs, coverage, and market-data management rather than agent-specific packaging. In practice, that makes it attractive for organizations that want to build their own AI architecture on top of a robust enterprise data backbone instead of depending on vendor-supplied MCP experiences. That is an inference from Xignite&#8217;s public positioning around cloud APIs, data management, and unlimited-usage commercial structure.</p><p><strong>3. Flexible delivery for production-scale systems</strong><br>Xignite supports multiple delivery modes across real-time, delayed, historical, and streaming-style services, and it explicitly markets itself for demanding display applications, backtesting, alerts, and application integration. That flexibility matters in AI systems because not all components need the same data path: one model might need historical fundamentals, another might need event-driven market updates, and a third might need reference data normalization.</p><h3>In short</h3><p>Xignite is not the most visibly AI-marketed provider in this group, but it is a serious contender for enterprise AI finance stacks. If your goal is to build a proprietary agent platform on top of large-scale, normalized market-data services, Xignite&#8217;s breadth and infrastructure orientation make it more compelling than its relative lack of public AI branding might suggest.</p><div><hr></div><h2>EOD Historical Data</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!W-VS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F488ecb5a-a062-4fc1-8602-441173a0ea4a_2381x1229.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!W-VS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F488ecb5a-a062-4fc1-8602-441173a0ea4a_2381x1229.png 424w, https://substackcdn.com/image/fetch/$s_!W-VS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F488ecb5a-a062-4fc1-8602-441173a0ea4a_2381x1229.png 848w, https://substackcdn.com/image/fetch/$s_!W-VS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F488ecb5a-a062-4fc1-8602-441173a0ea4a_2381x1229.png 1272w, https://substackcdn.com/image/fetch/$s_!W-VS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F488ecb5a-a062-4fc1-8602-441173a0ea4a_2381x1229.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!W-VS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F488ecb5a-a062-4fc1-8602-441173a0ea4a_2381x1229.png" width="1456" height="752" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/488ecb5a-a062-4fc1-8602-441173a0ea4a_2381x1229.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:752,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1281074,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/190589167?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F488ecb5a-a062-4fc1-8602-441173a0ea4a_2381x1229.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!W-VS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F488ecb5a-a062-4fc1-8602-441173a0ea4a_2381x1229.png 424w, https://substackcdn.com/image/fetch/$s_!W-VS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F488ecb5a-a062-4fc1-8602-441173a0ea4a_2381x1229.png 848w, https://substackcdn.com/image/fetch/$s_!W-VS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F488ecb5a-a062-4fc1-8602-441173a0ea4a_2381x1229.png 1272w, https://substackcdn.com/image/fetch/$s_!W-VS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F488ecb5a-a062-4fc1-8602-441173a0ea4a_2381x1229.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Overview</h3><p><a href="https://eodhd.com/">EOD Historical Data</a>, now commonly presented as EODHD, offers a broad financial data platform spanning fundamentals, historical end-of-day prices, live and real-time feeds, intraday data, U.S. options, financial news, stock screeners, technical indicators, and exchange/reference datasets. On its homepage, the company positions itself as a &#8220;one-stop shop&#8221; for 30+ years of historical, fundamental, and real-time data across global markets, with coverage figures including 60 stock exchanges and 150,000 tickers.</p><p>One of EODHD&#8217;s strengths is that it sits between lightweight developer tools and more professional research infrastructure. It offers structured JSON and CSV responses, coding libraries, spreadsheet add-ons, and a broad menu of market datasets without being limited to only one narrow workflow. It also exposes precomputed technical indicators through API endpoints rather than requiring users to calculate everything from raw time series.</p><p>This combination makes EODHD particularly attractive for builders who want reasonably broad market-data coverage and analytics features in a format that remains accessible to smaller teams, solo developers, and applied AI prototypes.</p><h3>What makes it valuable in the AI Agent era?</h3><p><strong>1. Official MCP support for agent integration</strong><br>EODHD provides an official MCP server for financial data and explicitly documents how to connect it to ChatGPT, Claude, and custom AI agents. The company describes this as a way for AI agents and LLMs to access real-time and historical financial data directly through MCP, making EODHD one of the clearest AI-era data providers alongside Alpha Vantage and Tradier.</p><p><strong>2. An official ChatGPT-oriented financial assistant</strong><br>Beyond MCP, EODHD also offers an official Financial Assistant for ChatGPT, which it describes as an AI that can generate code for EODHD APIs and provide finance insights grounded in real data and news. That does not just signal marketing interest in AI; it suggests the company is actively shaping its product and developer experience around LLM-driven usage patterns.</p><p><strong>3. Strong structured outputs plus higher-level analytics</strong><br>EODHD&#8217;s AI relevance is also practical. It provides structured JSON/CSV outputs, extensive API documentation, libraries, and technical-indicator endpoints that already package financial signals into machine-usable form. For agentic systems, that reduces the burden of transforming raw market data before it can be used in screening, summarization, ranking, or recommendation workflows.</p><h3>In short</h3><p>EODHD is one of the strongest all-around options for the AI agent era. It combines broad market coverage with precomputed indicators, developer-friendly structured data, an official MCP server, and a ChatGPT-oriented assistant. For teams that want something more AI-forward than classic enterprise vendors but broader than a narrow single-purpose API, EODHD is a very strong choice.</p><div><hr></div><h2>QuoteMedia</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!T3Qn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a158f85-6722-4e0c-8fb4-5eb4a4b119c6_2613x1409.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!T3Qn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a158f85-6722-4e0c-8fb4-5eb4a4b119c6_2613x1409.png 424w, https://substackcdn.com/image/fetch/$s_!T3Qn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a158f85-6722-4e0c-8fb4-5eb4a4b119c6_2613x1409.png 848w, https://substackcdn.com/image/fetch/$s_!T3Qn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a158f85-6722-4e0c-8fb4-5eb4a4b119c6_2613x1409.png 1272w, https://substackcdn.com/image/fetch/$s_!T3Qn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a158f85-6722-4e0c-8fb4-5eb4a4b119c6_2613x1409.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!T3Qn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a158f85-6722-4e0c-8fb4-5eb4a4b119c6_2613x1409.png" width="1456" height="785" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4a158f85-6722-4e0c-8fb4-5eb4a4b119c6_2613x1409.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:785,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1613479,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/190589167?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a158f85-6722-4e0c-8fb4-5eb4a4b119c6_2613x1409.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!T3Qn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a158f85-6722-4e0c-8fb4-5eb4a4b119c6_2613x1409.png 424w, https://substackcdn.com/image/fetch/$s_!T3Qn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a158f85-6722-4e0c-8fb4-5eb4a4b119c6_2613x1409.png 848w, https://substackcdn.com/image/fetch/$s_!T3Qn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a158f85-6722-4e0c-8fb4-5eb4a4b119c6_2613x1409.png 1272w, https://substackcdn.com/image/fetch/$s_!T3Qn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a158f85-6722-4e0c-8fb4-5eb4a4b119c6_2613x1409.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Overview</h3><p><a href="https://quotemedia.com/">QuoteMedia</a> is a long-standing market-data provider focused on real-time and historical data, news, analytics, and financial information for brokerages, websites, trading systems, and investor-facing products. Its Request APIs and OnDemand services are built around cloud-based access to market data, while its streaming products emphasize tick-by-tick delivery, low latency, and enterprise-grade reliability. QuoteMedia also highlights broad operational scale, including 110+ global exchanges, 200+ data APIs, 99.99% uptime, and 100+ news providers.</p><p>A notable strength of QuoteMedia is delivery flexibility. Its platform spans REST-style OnDemand APIs, WebSocket and other streaming interfaces, and SFTP-based file services for bulk delivery. It also supports JSON, XML, CSV, option-chain data, company profiles, historical time series, filings, and custom calculations. That makes QuoteMedia less of a single API product and more of a market-data delivery platform.</p><p>QuoteMedia&#8217;s public positioning is similar to Xignite in one important way: it is more infrastructure-oriented than explicitly LLM-oriented. In other words, its clearest strengths are reliability, breadth, delivery options, and integration into financial products, not public MCP or agent-marketing. That is an inference from the official materials reviewed.</p><h3>What makes it valuable in the AI Agent era?</h3><p><strong>1. Low-latency data for real-time agent monitoring</strong><br>QuoteMedia&#8217;s streaming stack is designed for real-time or delayed tick-by-tick data, normalized for ease of use and optimized for single-digit millisecond performance. For AI systems that monitor live markets, score signals, or trigger alerts and workflows off intraday movement, that kind of delivery profile is highly relevant.</p><p><strong>2. Multiple delivery modes for different agent architectures</strong><br>Modern AI finance stacks are not monolithic. Some components work best with REST requests, others with streams, and others with bulk files for offline training or evaluation. QuoteMedia supports cloud REST APIs, streaming APIs, and SFTP/file services, which makes it well suited to organizations building layered pipelines that combine real-time agent behavior with batch analytics and historical model development.</p><p><strong>3. Strong fit as a production data layer</strong><br>QuoteMedia offers market data, news, analytics, company profiles, option chains, filings, and historical data in structured formats such as JSON, XML, and CSV. That breadth makes it a useful foundation for internal copilots, research dashboards, summarization systems, and client-facing financial applications where the &#8220;AI&#8221; layer is built on top of the data platform rather than bundled by the vendor itself.</p><h3>In short</h3><p>QuoteMedia is a strong candidate for teams that care more about production-grade delivery and integration flexibility than about whether the vendor has already branded itself around AI agents. In the AI agent era, that still matters a lot: a reliable, low-latency, multi-format market-data backbone can be more valuable than flashy AI positioning if you are building your own orchestration layer.</p><div><hr></div><h2>Conclusion</h2><p>If the goal is to find the most AI-ready providers, Alpha Vantage, Tradier, and EODHD stand out because they already offer MCP or LLM-oriented support. Alpha Vantage is particularly strong for AI-native research tools, Tradier is strong for brokerage-connected agents, and EODHD is a strong general-purpose choice.</p><p>If the goal is enterprise-grade infrastructure for proprietary AI systems, Xignite and QuoteMedia remain highly relevant. They may be less visibly AI-marketed, but they are strong as scalable market data backbones.</p><p>So in the AI agent era, the best stock market data API depends on what you are building. For AI-native financial research, Alpha Vantage has a strong edge. For execution-oriented agents, Tradier stands out. For broad AI-enabled workflows, EODHD is highly competitive. For enterprise infrastructure, Xignite and QuoteMedia are still important players.</p>]]></content:encoded></item><item><title><![CDATA[7 SQL Use Cases Every Data Professional Should Know]]></title><description><![CDATA[Most people learn SQL as syntax. The real thing is knowing what kinds of problems it helps you solve.]]></description><link>https://www.nb-data.com/p/7-sql-use-cases-every-data-professional</link><guid isPermaLink="false">https://www.nb-data.com/p/7-sql-use-cases-every-data-professional</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Sat, 07 Mar 2026 12:19:16 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!9orW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bbb6184-6b0e-45de-9ab4-0f7ec96007de_1376x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9orW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bbb6184-6b0e-45de-9ab4-0f7ec96007de_1376x768.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9orW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bbb6184-6b0e-45de-9ab4-0f7ec96007de_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!9orW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bbb6184-6b0e-45de-9ab4-0f7ec96007de_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!9orW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bbb6184-6b0e-45de-9ab4-0f7ec96007de_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!9orW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bbb6184-6b0e-45de-9ab4-0f7ec96007de_1376x768.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9orW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bbb6184-6b0e-45de-9ab4-0f7ec96007de_1376x768.png" width="1376" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9bbb6184-6b0e-45de-9ab4-0f7ec96007de_1376x768.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1376,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2007256,&quot;alt&quot;:&quot;7 SQL Use Cases Every Data Professional Should Know&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/190182290?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bbb6184-6b0e-45de-9ab4-0f7ec96007de_1376x768.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="7 SQL Use Cases Every Data Professional Should Know" title="7 SQL Use Cases Every Data Professional Should Know" srcset="https://substackcdn.com/image/fetch/$s_!9orW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bbb6184-6b0e-45de-9ab4-0f7ec96007de_1376x768.png 424w, https://substackcdn.com/image/fetch/$s_!9orW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bbb6184-6b0e-45de-9ab4-0f7ec96007de_1376x768.png 848w, https://substackcdn.com/image/fetch/$s_!9orW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bbb6184-6b0e-45de-9ab4-0f7ec96007de_1376x768.png 1272w, https://substackcdn.com/image/fetch/$s_!9orW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bbb6184-6b0e-45de-9ab4-0f7ec96007de_1376x768.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A lot of people learn SQL in a frustrating way.</p><p>They start with <code>SELECT</code>, <code>FROM</code>, <code>WHERE</code>, <code>GROUP BY</code>, maybe a few joins, and if they stay long enough, a window function or two. They can write queries. They can pass the exercises. But when they face a real business question, they still freeze.</p><p>That usually happens because they learned SQL as a list of clauses instead of a way to think.</p><p>In real work, SQL is rarely about showing that you remember syntax. It is about knowing what question arises once it hits the data. Questions such as:</p><ul><li><p>Is this a reporting problem? </p></li><li><p>A funnel problem? </p></li><li><p>A cohort problem? </p></li><li><p>A segmentation problem? </p></li><li><p>A QA problem? </p></li></ul><p>The moment you can recognize that, SQL becomes much less intimidating and much more useful. That is the shift that matters.</p><p>The people who get genuinely strong at SQL are usually not the people who memorize the most functions. They are the people who can look at a business question and quickly understand what kind of data transformation it needs.</p><p>So instead of thinking about SQL as &#8220;a language I should know,&#8221; I think it is more useful to think about it as a toolkit for a handful of recurring jobs.</p><p>Here are seven of the most important ones. Let&#8217;s get into it.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2>1. KPI reporting</h2><p>When teams want to know what is happening in the business, they usually start with some version of a KPI question. Revenue by month. Daily active users. Orders by country. Average order value. Churn rate by plan. Refund rate by product. These are not flashy questions, but they are the foundation of most reporting work.</p><p>This is where SQL starts becoming practical. You are not trying to prove how advanced you are. You are trying to turn raw data into something clear enough for another person to act on.</p><p>That means defining the metric carefully, filtering the right time window, grouping at the right level, and returning a result that is readable. The technical tools are simple, but the judgment behind them matters a lot.</p><p>A lot of people underestimate this kind of SQL because it feels too basic. I think that is a mistake. A team with weak KPI logic usually ends up with weak everything else.</p><p>A simple example is monthly revenue by product category:</p><pre><code>SELECT
    DATE_TRUNC(&#8217;month&#8217;, order_date) AS order_month,
    product_category,
    SUM(revenue) AS total_revenue
FROM orders
WHERE order_date &gt;= DATE &#8216;2026-01-01&#8217;
GROUP BY 1, 2
ORDER BY 1, 3 DESC;</code></pre><p>This is a basic grouped summary, but that is exactly why it matters. A lot of useful SQL is just good filtering, clean aggregation, and returning a table that another person can use.</p><h2>2. Funnel analysis</h2><p>The second major use case is figuring out where people drop off.</p><p>This is where SQL starts feeling very close to product and growth work. A funnel question usually sounds like this: how many users started onboarding, how many completed profile setup, how many created their first project, and how many upgraded? In ecommerce, the same question shows up as view product, add to cart, begin checkout, and pay.</p><p>What makes funnel analysis valuable is that it shows where interest turns into friction.</p><p>A lot of the time, the problem is not &#8220;traffic is low.&#8221; The problem is that the path breaks at one specific step. SQL helps you see that step clearly. It lets you move from a vague sense that &#8220;conversion feels weak&#8221; to a more precise question like &#8220;why do so many users disappear between signup and first action?&#8221;</p><p>A simple event-based funnel might look like this:</p><pre><code>SELECT
    step_name,
    COUNT(DISTINCT user_id) AS users_at_step
FROM onboarding_events
WHERE event_date &gt;= DATE &#8216;2026-03-01&#8217;
GROUP BY 1
ORDER BY
    CASE step_name
        WHEN &#8216;signup&#8217; THEN 1
        WHEN &#8216;verify_email&#8217; THEN 2
        WHEN &#8216;create_project&#8217; THEN 3
        WHEN &#8216;first_active_use&#8217; THEN 4
    END;</code></pre><p>This is not the most advanced funnel query in the world, but it already gives you a clearer conversation. Instead of saying &#8220;activation is weak,&#8221; you can ask, &#8220;Why do so many users disappear between verification and first project creation?&#8221;</p><p>Once you can answer that, the conversation gets much more useful.</p><h2>3. Cohort retention analysis</h2><p>This is one of the most important SQL use cases because it forces better thinking.</p><p>A cohort retention analysis groups users by a shared starting point, then checks whether they come back in later periods. That sounds simple, but it is one of those areas where small definition choices change the whole story. What puts a user into a cohort? What counts as a return? What does a week mean? Should a user count once per week or every time they generate an event?</p><p>That is why good retention work is not mainly about writing SQL. It is about locking the logic before the SQL ever begins.</p><p>This is also where SQL becomes more than a reporting language. It becomes a way of expressing lifecycle behavior. Once you can build a trustworthy retention table, you can stop asking &#8220;are users coming back?&#8221; in a vague way and start asking &#8220;which users are sticking, when do they drop, and what changed across cohorts?&#8221;</p><p>That is one of the reasons I like this use case so much. It pushes people past syntax into actual analytical design.</p><p>A very small example of the logic looks like this:</p><pre><code>WITH user_cohort AS (
    SELECT
        user_id,
        DATE_TRUNC(&#8217;week&#8217;, MIN(login_date)) AS cohort_week
    FROM logins
    GROUP BY 1
),
user_activity AS (
    SELECT
        l.user_id,
        DATE_TRUNC(&#8217;week&#8217;, l.login_date) AS activity_week
    FROM logins l
    GROUP BY 1, 2
)
SELECT
    c.cohort_week,
    a.activity_week,
    COUNT(DISTINCT a.user_id) AS active_users
FROM user_cohort c
JOIN user_activity a
  ON c.user_id = a.user_id
GROUP BY 1, 2
ORDER BY 1, 2;</code></pre><p>This is only the skeleton, not the full retention table. But even here, you can already see the shape: assign the cohort, map later activity, then aggregate by period.</p><p>You can check the deep dive of this use case here:</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;a0951efb-026e-4d0d-aeaf-8731e89e2fbc&quot;,&quot;caption&quot;:&quot;Most retention tables are not wrong because the SQL is complicated.&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;lg&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Cohort Retention in SQL&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:6000855,&quot;name&quot;:&quot;Cornellius Yudha Wijaya&quot;,&quot;bio&quot;:&quot;Sharing Data Knowledge to improve your values&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/583076b2-657b-44bf-8aa9-9263e5bf04f0_544x544.png&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2026-03-06T18:39:19.710Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!5opy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.nb-data.com/p/cohort-retention-in-sql&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:190126571,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:1,&quot;comment_count&quot;:0,&quot;publication_id&quot;:37262,&quot;publication_name&quot;:&quot;Non-Brand Data&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!06DP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6c0e1cde-d120-4029-8ffd-2a8c7c6e4504_1280x1280.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><h2>4. Segmentation</h2><p>Once you know the overall number, the next question is almost always: who exactly is driving it?</p><p>That is segmentation.</p><p>Averages are useful, but they hide a lot. SQL becomes much more powerful once you stop treating all users as one group and start cutting the data into meaningful slices. That might mean country, plan, acquisition channel, device type, power users versus casual users, or first purchase month.</p><p>And in practice, this is where a lot of strong SQL users separate themselves. They stop producing one big average and start showing where the business behaves differently across groups.</p><p>A simple segmentation example might be conversion rate by acquisition channel:</p><pre><code>SELECT
    acquisition_channel,
    COUNT(DISTINCT user_id) AS users,
    SUM(CASE WHEN converted = 1 THEN 1 ELSE 0 END) AS converted_users,
    ROUND(
        1.0 * SUM(CASE WHEN converted = 1 THEN 1 ELSE 0 END)
        / COUNT(DISTINCT user_id),
        3
    ) AS conversion_rate
FROM user_conversion_summary
GROUP BY 1
ORDER BY conversion_rate DESC;</code></pre><p>This is where SQL starts feeling strategic. You stop asking, &#8220;Is conversion improving?&#8221; and start asking, &#8220;Is conversion improving for the users we actually care about?&#8221;</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/7-sql-use-cases-every-data-professional?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/7-sql-use-cases-every-data-professional?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><h2>5. Experiment analysis</h2><p>If you work near product or growth teams, SQL becomes very important the moment experiments show up.</p><p>Before anyone talks about significance, lift, or confidence intervals, someone still has to build the dataset properly. Who was in the control group? Who was in the treatment group? Who converted? Over what window? Were there logging issues? Did the assignment logic work as expected?</p><p>A lot of that early work is SQL.</p><p>And this matters more than people think, because if the experiment table is wrong, everything that comes after it is already compromised. If the assignment table is joined incorrectly, if the outcome window is inconsistent, or if duplicated rows quietly inflate conversions, the eventual statistical discussion becomes much less meaningful.</p><p>So even though experiment analysis sounds advanced, a lot of it still comes down to careful SQL habits and clean dataset construction.</p><p>A simple experiment summary might look like this:</p><pre><code>SELECT
    variant,
    COUNT(DISTINCT user_id) AS users,
    SUM(CASE WHEN purchased = 1 THEN 1 ELSE 0 END) AS purchasers,
    ROUND(
        1.0 * SUM(CASE WHEN purchased = 1 THEN 1 ELSE 0 END)
        / COUNT(DISTINCT user_id),
        3
    ) AS purchase_rate
FROM experiment_user_summary
WHERE experiment_name = &#8216;checkout_redesign_v1&#8217;
GROUP BY 1
ORDER BY 1;</code></pre><p>That is not the full experiment analysis, but it is the foundation.</p><h2>6. Data quality and QA checks</h2><p>This is one of the least glamorous SQL use cases, and one of the most valuable.</p><p>A huge amount of trust in data work comes from catching bad structure early. Duplicate rows. Missing keys. Broken joins. Sudden changes in counts. Tables that stopped updating. Records that should be impossible but somehow exist anyway.</p><p>SQL is excellent for this kind of work because it is good at isolating patterns, comparing counts, checking coverage, and surfacing anomalies before they become reporting problems.</p><p>This is also one of the places where data professionals become more mature in practice. They stop using SQL only to answer the question they were asked, and they start using SQL to challenge whether the dataset itself deserves trust.</p><p>That is a very different mindset.</p><p>Once you develop it, your work usually becomes much more reliable.</p><p>For example, if you want to check for duplicate order IDs:</p><pre><code>SELECT
    order_id,
    COUNT(*) AS row_count
FROM orders
GROUP BY 1
HAVING COUNT(*) &gt; 1
ORDER BY row_count DESC;</code></pre><p>This is basic, but incredibly useful. </p><h2>7. Operational monitoring</h2><p>The last use case is the one that makes SQL feel closest to the day-to-day operating layer of a business.</p><p>Sometimes the question is not &#8220;what happened this quarter?&#8221; Sometimes the question is &#8220;did the pipeline run?&#8221;, &#8220;are transactions missing?&#8221;, &#8220;did yesterday&#8217;s volume collapse?&#8221;, or &#8220;did a critical table stop refreshing?&#8221;</p><p>At that point, SQL is not just helping with analysis. It is helping keep the system honest.</p><p>This kind of work often lives somewhere between analytics, operations, and data engineering. You are comparing expected versus actual counts, checking daily or weekly movement, and trying to spot problems before somebody else finds them in a broken dashboard or an angry meeting.</p><p>If you only think of SQL as a tool for reports, you miss how often it becomes part of the business&#8217;s operational nervous system.</p><p>A simple monitoring query might compare day-over-day order counts:</p><pre><code>SELECT
    order_date,
    COUNT(*) AS orders_today,
    LAG(COUNT(*)) OVER (ORDER BY order_date) AS orders_yesterday
FROM orders
GROUP BY 1
ORDER BY 1;</code></pre><p>This is where window functions become especially useful. They let you compare each row to related rows while keeping the row-level result visible, which is exactly the kind of thing you want for trend and monitoring work. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/7-sql-use-cases-every-data-professional/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/7-sql-use-cases-every-data-professional/comments"><span>Leave a comment</span></a></p><h2>The bigger point</h2><p>If you look across all seven use cases, the pattern is pretty clear.</p><p>SQL is rarely valuable because of its isolated syntax.</p><p>It is valuable because the same small set of ideas keeps getting reused across real work.</p><p>That is why strong SQL users usually do not sound like they are reciting functions. They sound like they understand data shape.</p><p>That is a much better goal than &#8220;learn more SQL syntax.&#8221;</p><h2>Where to go next</h2><p>If you are still early, I would not try to learn every advanced clause in one sitting.</p><p>I would focus on connecting SQL to actual problems.</p><p>That is exactly why I built the SQL track into the NBD Focus Map. The point is not to learn SQL randomly. The point is to see how the pieces fit together and start shipping small, useful work with them.</p><h2>Start here</h2><p>If you want the broader path, start with the <strong>Focus Map</strong>:<br></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/p/nbd-focus-map-free-pdf&quot;,&quot;text&quot;:&quot;Focus Map&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/p/nbd-focus-map-free-pdf"><span>Focus Map</span></a></p><p><br>If you want the full paid system, use:</p><ul><li><p><strong>Vault:</strong> <a href="https://www.nb-data.com/p/nbd-reading-vault-paid-guided-paths">https://www.nb-data.com/p/nbd-reading-vault-paid-guided-paths</a></p></li><li><p><strong>Template Index:</strong> <a href="https://www.nb-data.com/p/template-pack-index-paid">https://www.nb-data.com/p/template-pack-index-paid</a></p></li><li><p><strong>Subscriber Benefits:</strong> <a href="https://www.nb-data.com/p/subscriber-benefits?utm_source=chatgpt.com">https://www.nb-data.com/p/subscriber-benefits</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Cohort Retention in SQL]]></title><description><![CDATA[From Raw Events to a Decision-Ready Table]]></description><link>https://www.nb-data.com/p/cohort-retention-in-sql</link><guid isPermaLink="false">https://www.nb-data.com/p/cohort-retention-in-sql</guid><dc:creator><![CDATA[Cornellius Yudha Wijaya]]></dc:creator><pubDate>Fri, 06 Mar 2026 18:39:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!5opy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5opy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5opy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg 424w, https://substackcdn.com/image/fetch/$s_!5opy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg 848w, https://substackcdn.com/image/fetch/$s_!5opy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!5opy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5opy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg" width="1312" height="736" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:736,&quot;width&quot;:1312,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:89400,&quot;alt&quot;:&quot;Cohort Retention in SQL&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.nb-data.com/i/190126571?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Cohort Retention in SQL" title="Cohort Retention in SQL" srcset="https://substackcdn.com/image/fetch/$s_!5opy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg 424w, https://substackcdn.com/image/fetch/$s_!5opy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg 848w, https://substackcdn.com/image/fetch/$s_!5opy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!5opy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e76dc69-8ad0-449f-8f74-a8b8885dbdd9_1312x736.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Most retention tables are not wrong because the SQL is complicated.</p><p>They are wrong because the definitions are loose.</p><p>Someone says, &#8220;Let&#8217;s look at retention,&#8221; a query gets written, a heatmap shows up in a dashboard, and suddenly everyone is talking about Week 1 and Month 1 as if those numbers are objective facts. They usually are not. They are the result of choices. What counts as the start of a user&#8217;s journey? What counts as a return? What exactly is a week? What timezone are we using? Are we measuring one user once per period, or accidentally counting heavy users multiple times?</p><p>That is the real work in cohort retention. Not the division. Not the pivot table. The real work is deciding what story the table is allowed to tell.</p><p>At its core, cohort analysis is simple. You group users by a shared starting point, then measure what those users do in later periods. That is the common backbone behind most cohort SQL tutorials and warehouse implementations.</p><p>What makes it tricky is that small choices can change the story enough to change the decision.</p><p>So in this piece, I want to show you how I think about cohort retention in SQL when I want something that is not just presentable, but actually trustworthy. We will walk through a small sample dataset, turn it into a retention table step by step, and discuss the parts that often go wrong: cohort definition, return-event design, week boundaries, duplicate activity, partial cohorts, and interpretation.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.nb-data.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.nb-data.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><h2>Start with the question, not the query</h2><p>Before touching SQL, I like to ask one uncomfortable question:</p><p><strong>What exactly do I want this retention table to help me decide?</strong></p><p>That question matters because different cohort definitions answer different business questions.</p><p>If I group users by the week they signed up, I am usually asking something about onboarding, activation, or acquisition quality. I want to know whether new users are sticking around after entering the funnel.</p><p>If I group users by the week they first did something meaningful, I am asking something slightly different. I am saying that signup is not the real beginning of value. Maybe the real beginning is the first login, the first purchase, the first report built, or the first document uploaded. In that case, I am less interested in the funnel entry and more interested in what happens once a user actually starts using the product.</p><p>Both are valid. But they are not interchangeable.</p><p>The same holds for the return event. If I define retention as &#8220;any page view,&#8221; my table might look reassuring while hiding the fact that users are not doing anything meaningful. If I define retention as &#8220;purchase,&#8221; the metric might be more valuable but also much sparser. There is no universally correct event. There is only one event that is more or less aligned with the value loop you care about.</p><p>Then there is the time bucket. This is the part people often treat as neutral, even though it really isn&#8217;t. A daily retention table tells a different story than a weekly one. A weekly table tells a different story than a monthly one. And even the idea of a &#8220;week&#8221; is less fixed than people think. BigQuery, for example, distinguishes between <code>WEEK</code>, <code>WEEK(&lt;WEEKDAY&gt;)</code>, and <code>ISOWEEK</code>, and those choices affect how dates are grouped and how period differences are calculated.</p><p>That is why I think of cohort retention as a design problem before I think of it as a SQL problem.</p><h2>The version we&#8217;re building here</h2><p>To make this concrete, let&#8217;s keep the example small and explicit.</p><p>In this walkthrough:</p><ul><li><p>A user&#8217;s cohort is the <strong>week of their first login</strong></p></li><li><p>Retention means they performed a <strong>login</strong> in a later week</p></li><li><p>The table uses <strong>calendar weeks</strong></p></li><li><p>Each user should count at most <strong>once per week</strong></p></li></ul><p>That last condition matters a lot. If a user logs in ten times in the same week, they are still one retained user for that week. Retention is about whether someone came back in the period, not how noisy their event stream was.</p><h2>Sample data</h2><p>Here is a tiny events table we can use end-to-end.</p>
      <p>
          <a href="https://www.nb-data.com/p/cohort-retention-in-sql">
              Read more
          </a>
      </p>
   ]]></content:encoded></item></channel></rss>