<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title>Data Engineering Blog &amp; Second Brain</title>
        <link>https://www.ssp.sh/</link>
        <description>Top-ranked Data Engineering Blog with Genuine Insights into the Data Ecosystem with 1000s of Second Brain notes and an online book on Patterns of Data Engineering, all connected with interactive graphs.</description>
        <generator>Hugo -- gohugo.io</generator><language>en-US</language><managingEditor>hello@sspaeti.com (Simon Späti)</managingEditor>
            <webMaster>hello@sspaeti.com (Simon Späti)</webMaster><copyright>All rights reserved. Sharing of excerpts with proper attribution is encouraged for non-commercial purposes. For commercial use or republication, please contact hello@sspaeti.com.</copyright><lastBuildDate>Thu, 16 Jul 2026 08:31:17 &#43;0100</lastBuildDate>
            <atom:link href="https://www.ssp.sh/index.xml" rel="self" type="application/rss+xml" />
        <item>
    <title>The Act and the Outcome of Creation</title>
    <link>https://www.ssp.sh/blog/on-creation/</link>
    <pubDate>Thu, 16 Jul 2026 08:31:17 &#43;0100</pubDate>
    <author>Simon Späti</author>
    <guid>https://www.ssp.sh/blog/on-creation/</guid><enclosure url="https://www.ssp.sh/blog/on-creation/featured-image.jpg" type="image/jpeg" length="0" /><description><![CDATA[<p>Creation is the ultimate form of pursuing ourselves, giving to the world when shared, and using the power of our subconscious. It gives us joy, and to every artist, it is the ultimate (flow) state of happiness.</p>
<h2 id="the-act-of-creation">The Act of Creation</h2>
<p>The act of creation is an outlet. It gives joy to us when we create something out of nothing, we block out anxiety or boredom.</p>
<p>Creating should be done like:</p>
<ul>
<li><strong>a kid in mind</strong>: effortless, exploring your thoughts, and seeing where it leaves you, as Picasso said: He wouldn&rsquo;t have bothered to start a painting if he knew the outcome.</li>
<li><strong>a play</strong><sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>: flawless and free.</li>
<li><strong>calm like the water</strong>: flowing wherever your mind is going.</li>
<li><strong>to fulfill yourself</strong>: by portraying your never-resting thoughts. Share it with the world. Create with joy and love in mind.</li>
<li><strong>follow your train of thought</strong>: see where it leads you. You start somewhere, on an unfinished emotion, a task, or anything to ponder on.</li>
</ul>
<p>Creation, and especially writing, can help <em>resolve the unresolved</em>.</p>
<h3 id="creation-to-make-a-difference-joy-and-laughter">Creation to Make a Difference: Joy and Laughter</h3>
<p>Creating is also about <strong>making a difference</strong>. If nothing changes in you, the people, not enough value was added to your creation. Any creation should trigger something. But foremost, the process of creation should change something in you, some release, a good feeling, it must feel right. If it does nothing, an empty feeling, the creation might not be there yet.</p>
<p>Create with <strong>love and empathy</strong> in mind. Liking someone is one thing, but giving love, caring for someone, and integrating that into your work is the ultimate way to create.</p>
<p>Create <strong>joy and laughter</strong>. Speak the language of people, make them happy by creating enjoyment.</p>
<h3 id="following-our-emotions-without-boundaries">Following Our Emotions without Boundaries</h3>
<p>When we follow our emotions <strong>without any boundaries</strong>, following our instincts, we get into a state of deep focus and deep concentration where all thoughts can be resolved.</p>
<p>Creation is our inner [[Gut Feeling|Instinct]]. If we follow and nourish it, great things can arise from it. Treat it as an outlet for your body and mind. Go with the flow, follow along. Create for the sake of creation.</p>













  
<figure><a target="_blank" href="/blog/on-creation/creation-fountain-pen.webp" title="">

</a><figcaption class="image-caption">My outlet of writing this article, not knowing where it leads me, but just letting my thoughts out as they come, while sitting still in nature somewhere close to the beach in Italy.</figcaption>
</figure>
<h3 id="learn-for-life-follow-your-own-uniqueness">Learn for Life, Follow Your Own Uniqueness</h3>
<p>The act of creation is <strong>following your own life rhythm</strong>, for example, the [[Pathless Path]]. It is the ultimate form of connecting with yourself.</p>
<p>All creation is unique, unique specifically to you as the creator, but also (hopefully) to the reader, viewer, or consumer of your work. Your creations live long after your death; others will connect and create new works from them. It&rsquo;s giving joy after you&rsquo;re gone.</p>
<p><strong>Learn for Life</strong>, as I like to say, has been my earliest mantra when starting any of my creations shared online. The common definition of <em>success</em> is reaching a desired outcome. The Scientist&rsquo;s definition of success is: if you learn something new, you haven&rsquo;t failed, as Anne-Laure Le Cunff says on <a href="https://share.snipd.com/snip/c0b1451b-d924-42ce-8c8a-c8fc559d3925" target="_blank" rel="noopener noreffer">How to Design Tiny Experiments Like a Scientist</a>.</p>
<p>For example, a scientist defines it like this: if you say you want to write more, instead of just writing, you define &lsquo;I will write for at least one month, or at least 10 articles. And then you don&rsquo;t stop before that. Success is the experience, not the outcome.</p>
<h2 id="the-outcome-of-creation">The Outcome of Creation</h2>
<p>The act of creation is a <strong>slow process</strong> that needs time and experience. A slow life, an [[The Ordinary (Boring) Life|ordinary, boring life]] even, to focus on the details, using the craftsmanship refined over the years, and retrieving joy from the process of creation.</p>
<p>What matters is the quality and outcome we are proud to release to the world.</p>
<h3 id="gifts-shared-with-the-world">Gifts Shared with the World</h3>
<p>You create for yourself with <strong>no return in mind</strong>, you just share it as a gift for the world, for anyone to consume.</p>
<p>Creation thought of as a gift is an easy way for you to create without the burden of pleasing people, as it&rsquo;s take-it-or-leave-it, like a gift. No strings attached, just <strong>making gifts</strong>.</p>
<h3 id="a-curious-mind-is-mostly-a-subconscious-mind">A Curious Mind is Mostly a Subconscious Mind</h3>
<p>When we create, most of our <strong>subconscious is driving</strong> the thought. Just let it cruise and see what the outcome is. Use the conscious mind for fixing errors and making sure the sentences make sense later, but don&rsquo;t start with it.</p>













  
<figure><a target="_blank" href="/blog/on-creation/conscois-vs-subconcious.webp" title="">

</a><figcaption class="image-caption">Conscious mind vs subconscious and unconscious minds (5 to 95% difference) | Image from <a href="https://twitter.com/connibiesalski/status/1641821412238442496" target="_blank" rel="noopener noreffer">this Tweet</a>.</figcaption>
</figure>
<h3 id="the-spark-of-joy-philosophy">The Spark of Joy Philosophy</h3>
<p>Different flairs for designs to add ease or user delight to your creation. Similar to the &ldquo;<a href="https://github.com/swyxio/spark-joy" target="_blank" rel="noopener noreffer">Spark Joy Philosophy</a>&rdquo;, which is very fitting when creating for the enjoyment of the reader.</p>
<p>To me, creation <strong>sparks joy for myself</strong>, it&rsquo;s the outlet for me to release and <strong>organize my thoughts</strong>. It avoids the [[shallow happiness]] that I get from social media, binge-watching Netflix, or other brainless activities on the phone or TV. Sure, there&rsquo;s time for that too, to calm down, but if we are not careful, <a href="/blog/well-being-algorithms/" rel="">the algorithms</a> will take over and we default to always choosing the easy choice.</p>
<h2 id="the-reward-is-long-term">The Reward is Long-term</h2>
<p>Creating is hard, there&rsquo;s friction, it does not directly work, but the <strong>reward is long-term</strong>, with [[deep happiness]] and a deep flow state: &ldquo;It&rsquo;s magical that they just tried to be there&rdquo;, which is the highest form of happiness artists get (see <a href="https://www.goodreads.com/book/show/615570.The_Artist_s_Way" target="_blank" rel="noopener noreffer">The Artist&rsquo;s Way</a>) as we discovered in <a href="/blog/finding-flow" rel="">Finding Flow</a>, with escaping digital distractions through deep work and slow living.</p>
<p>I hope you find your outlet for creation, an act you can partake in to develop happiness and joy for yourself, getting into that deep flow and having a slow process where many small gifts of creation can be shared with the world.</p>
<p><em>This article was created with the inspiration of reading <a href="https://sive.rs/h" target="_blank" rel="noopener noreffer">How to Live</a> by Derek Sivers in Cavallino, Italy.</em></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I was watching kids play football on a campsite football field in Italy, so free and joyful.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
</item>
<item>
    <title>The Grammar of Data: Define Once, Run Anywhere with Cross-Engine Expressions</title>
    <link>https://www.ssp.sh/blog/grammar-data-engineering/</link>
    <pubDate>Wed, 08 Jul 2026 08:00:08 &#43;0200</pubDate>
    <author>Simon Späti</author>
    <guid>https://www.ssp.sh/blog/grammar-data-engineering/</guid><enclosure url="https://www.ssp.sh/blog/grammar-data-engineering/featured-image.png" type="image/png" length="0" /><description><![CDATA[<p>Grammars for languages or any other field are a beautiful thing. They compress complex systems into a language with a couple of rules. For the spoken language example, we know when to capitalize a letter or how to start a sentence. There are clear rules. Grammars also help us remember, as we do not need to recall every little rule, but apply them in a structured way.</p>
<p>For text editing, we have Vim motions that help us navigate a text document with 1000s of shortcuts, but because there is a grammar, we do not need to remember them all, but learn the structure of the grammar and combine them. But what if you work in data? What if we could have the same for data, a grammar for data engineering, or a language that defines it?</p>
<p><em>Expressing</em> our needs declaratively and decisively? Also, expressing it in a way that leads to reproducible outcomes, or works with multiple parts and execution engines already out there. This is what we will discuss in this article. How existing tooling, such as Ibis, provides some capabilities, and how xorq extends them by adding full lineage and transparency for humans, with included executable memory for useful tabular data, all manifested in a single git repository.</p>
<h2 id="expressions-for-data-engineering-workloads">Expressions for Data Engineering Workloads</h2>
<p>Having a <strong>grammar for data engineering</strong> means we can express the workloads in a <a href="https://www.ssp.sh/brain/declarative/" target="_blank" rel="noopener noreffer">declarative</a> manner, and then be sure we can deterministically reproduce and apply that exact definition.</p>
<p>It&rsquo;s similar to the concept of a <a href="https://www.rilldata.com/blog/the-rise-of-the-declarative-data-stack" target="_blank" rel="noopener noreffer">Declarative Data Stack</a> I introduced a while back, but it gives the stack not only configurations but also a language with in-built manifestation and execution engines.</p>













  
<figure><a target="_blank" href="/blog/grammar-data-engineering/write-manifest-execute.webp" title="">

</a><figcaption class="image-caption">Write -&gt; Manifest and Execute | Image from <a href="https://github.com/xorq-labs/xorq" target="_blank" rel="noopener noreffer">Composable expressions for data pipelines</a></figcaption>
</figure>
<p>In the above image, we see:</p>
<ol>
<li>How to <strong>express</strong> (write) our transformations and business logic. It&rsquo;s the context of every ML or DE pipeline.</li>
<li>We can build the <a href="https://xorq.dev/blog/expression-lifecycle/" target="_blank" rel="noopener noreffer">expression</a> into a <strong>manifest</strong> that has a unique hash, runs input validations, tracks lineage, creates a deterministic cache, and produces a human-readable <code>expr.yaml</code> you can diff and review in a PR.</li>
<li>Lastly, we can <strong>execute</strong> it in any execution engine with the <em>same manifest</em>.</li>
</ol>
<p>This is hugely powerful and separates the concerns of defining logic, verification in the manifest step, and execution as a composable data stack, as Wes McKinney <a href="https://open.spotify.com/episode/4yEBsHs75QyxnQqK11ghyC?si=2c7861fde2354a52" target="_blank" rel="noopener noreffer">called it</a>, with multi-compute engine possibilities.</p>
<h3 id="how-the-de-language-works-different-expression-types">How the DE Language Works: Different Expression Types</h3>
<p>Every grammar starts with nouns, and here the noun is the <strong>source</strong>, a node that holds data but carries no transformation yet. It might be an in-memory table, a registered connection to a warehouse, or just a lazy pointer to a file on disk that hasn&rsquo;t been read. They&rsquo;re simply <em>referenced</em>, the way a noun refers to a thing before any verb acts on it.</p>
<p>The verbs in our language are <strong>transforms</strong> such as <em>filter, select, mutate, aggregate, join, order, limit</em>. Each one takes a source (or another transformed expression) and returns a new, immutable expression. You do not mutate anything before it, only describe what should happen next.</p>
<p>Looking at a definition such as <code>.filter(...).aggregate(...).mutate(...)</code>, we can see this as a sentence. The moment a verb is applied, the expression stops being a plain noun and becomes a <em>statement</em>, a description of &ldquo;data plus what should happen to it.&rdquo; But the sentence isn&rsquo;t spoken yet, it stays inert, fully composed but unexecuted, until something finally asks it to run. That&rsquo;s the deferred part of the grammar: writing the sentence and saying it out loud are two different acts.</p>
<p>There&rsquo;s a third part of speech worth naming: the <strong>template</strong>. Instead of writing a sentence about a specific noun, you can write one about a noun&rsquo;s <em>shape</em>, a schema with no rows behind it. A template says &ldquo;given something with a column of this type, here is what I&rsquo;ll do to it,&rdquo; and only later gets bound to an actual source, at which point the placeholder resolves and it becomes an ordinary statement again.</p>
<p>And we have <strong>modifiers</strong> that ride alongside a statement without changing what it computes. They&rsquo;re small tags of metadata that say &ldquo;this expression also represents a fitted model&rdquo; or &ldquo;this is a saved reference to something else.&rdquo; It&rsquo;s like a footnote with additional metadata that doesn&rsquo;t change the surface meaning, but adds context for later use.</p>
<p>This analogy makes the grammar compose the same way regardless of which engine eventually executes it. There are more parts, but with just these four, noun, verb, template, modifier, you can read (and write) arbitrarily complex data pipelines the same way learning a handful of verb-and-object combinations in a text editor lets you compose arbitrarily complex edits.</p>
<blockquote>
<p>[!tip] Avoids building &ldquo;Inner-Platform Effect&rdquo; with repeated tools<br>
With this grammar, we can avoid repeatedly implementing the same logic we already have, but manifest and express our logic once, and reuse it with different execution engines, exactly what Ibis and xorq allow. Similar to what the <a href="https://en.wikipedia.org/wiki/Inner-platform_effect" target="_blank" rel="noopener noreffer">inner-platform effect</a> means for software best practices.</p>
</blockquote>
<h3 id="why-a-grammar-is-really-good-for-llms">Why a Grammar is Really Good for LLMs</h3>
<p>Having a grammar is really good for LLMs, too. It helps them first to declare data artifacts and second to execute them reproducibly.</p>
<p>On top, expressions can be LLM-agnostic, and we can interchange the LLMs we use just with an expression. Also, the chart is just an expression, or the data catalog and the metrics.</p>
<h2 id="model-once-represent-everywhere-expressing-the-full-data-stack-with-a-single-expression">Model Once, Represent Everywhere: Expressing the Full Data Stack with a Single Expression</h2>
<p>Like <a href="https://netflixtechblog.com/uda-unified-data-architecture-6a6aee261d8d" target="_blank" rel="noopener noreffer">UDA (Unified Data Architecture)</a> from Netflix, we define our expressions once and represent them everywhere. Netflix built UDA to solve <strong>duplicated models, inconsistent terminology, and siloed systems</strong>, where the same concept like &lsquo;actor&rsquo; or &lsquo;movie&rsquo; gets modeled differently across teams, with no shared foundation. Their answer was a full knowledge graph with a metamodel, making the conceptual model part of the actual control plane.</p>
<p>Not everyone needs Netflix-scale tooling, though. For a code-first approach, xorq gives you the same core principle: define once, execute anywhere by writing a declarative Ibis expression, serializing them as content-addressed YAML artifacts, and running against any supported engine, fully reproducible.</p>
<p>The difference worth noting: UDA is a <em>semantic</em> layer defining what data <em>means</em> across systems. Xorq is a <em>computational</em> layer defining what transformations <em>do</em> across engines. Both reject the same anti-pattern of re-implementing the same logic for every system.</p>
<h2 id="entering-xorq-the-horizontal-data-architecture">Entering Xorq: The Horizontal Data Architecture</h2>
<p>Xorq is an executable memory system for tabular data that works horizontally across your data stack, supporting everything from discovery with a catalog to defining transformation logic to modeling.</p>
<p>It has declarative transformation (Pandas style), and you can build ML pipelines and prepare data with its semantics in a single stack that is not vertically integrated, but horizontally integrated, giving your agents a catalog of executable pipelines and turning short-lived agent work such as wrangling scripts, sklearn pipelines, ad-hoc tables into durable, composable, executable artifacts that any future agent or human can discover, reproduce, and reuse.</p>













  
<figure><a target="_blank" href="/blog/grammar-data-engineering/horizontal-data-stack.webp" title="">

</a><figcaption class="image-caption">Old vertical siloed way vs. the horizontal composable data stack way with multi-engine</figcaption>
</figure>
<p>The horizontal data stack shows what Xorq brings to the table. <strong>Xorq&rsquo;s origins</strong> started from a <a href="https://xorq.dev/blog/bsl-xorq/" target="_blank" rel="noopener noreffer">git-native semantic layer</a>, for data analysts out of college, to build semantic models for a living, to make their lives easier.</p>
<p>From point-and-click tools, dragging tables and drawing joins manually, only to add more reporting tools on top to create pixel-perfect reports. Also performance-wise, it didn&rsquo;t scale, meaning we needed cubes to make it faster, adding another layer of complexity.</p>
<p>And there was no lineage that shows from source to dashboard. The question asked was: &ldquo;what if we could do this <strong>end-to-end data engineering workflow locally</strong>?&rdquo;. This is what the horizontal data stack and xorq are providing.</p>
<p>To add semantic layer capabilities, Julien Hurault and Hussain built the <a href="https://github.com/boringdata/boring-semantic-layer" target="_blank" rel="noopener noreffer">Boring Semantic Layer</a> + the Xorq catalog, providing a semantic model you define in Python, check into git, and query from the CLI.</p>
<h3 id="compressing-logic-into-a-single-executable">Compressing Logic into a Single Executable</h3>
<p>Compression of a full data stack into a single executable is hard, but xorq tries exactly this with the help of Ibis, git, uv, and DataFusion.</p>
<p>The <strong>design choices</strong> of xorq showcase even better what it is, and what they enable:</p>
<ul>
<li><strong><a href="https://ibis-project.org/" target="_blank" rel="noopener noreffer">Ibis</a> as expression layer</strong> (v9.5.0+, partial): Declarative dataframe expressions compiled to multiple backends (xorq supports a subset of the Ibis API, not the full surface)</li>
<li><strong><a href="https://git-scm.com/" target="_blank" rel="noopener noreffer">Git</a> for state and storage</strong>: The catalog is a git repo of entries with git-annex support for large files</li>
<li><strong><a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener noreffer">uv</a> for reproducible environments</strong>: Each entry ships with a wheel and pinned <code>requirements.txt</code>.</li>
<li><strong><a href="https://datafusion.apache.org/" target="_blank" rel="noopener noreffer">DataFusion</a> for embedded compute</strong>: Pipelines execute in-process with SQL and UDFs</li>
</ul>
<h3 id="composable-data-engines">Composable Data Engines</h3>
<p>Another big advantage of expressions and having a grammar for data engineering is easily <a href="https://docs.xorq.dev/getting_started/switch_backends.html" target="_blank" rel="noopener noreffer">switching between backends</a>, with no change to the transformation or business logic. It&rsquo;s just defining the backend from Apache Arrow Flight to DuckDB or any other engine.</p>
<p>We write the definitions and express our tabular data and computations. The engine, in this case xorq, can build it into a manifest file that is deterministic and hashed.</p>
<p>Xorq uses Ibis as the expression layer for single-backend logic, then builds the <strong>cross-engine expression tree</strong> into a serialized YAML artifact. When moving data between backends, xorq transfers Apache Arrow <a href="https://docs.rs/arrow/latest/arrow/record_batch/struct.RecordBatch.html" target="_blank" rel="noopener noreffer">RecordBatch</a> streams between them—each backend acts as a RecordBatch transducer. No CSV serialization, no JSON encoding needed. This makes backend switching fast and memory-efficient. Write declarative Ibis expressions that run like a tool—xorq extends Ibis with caching, <a href="https://docs.xorq.dev/concepts/understanding_xorq/multi_engine_execution.html" target="_blank" rel="noopener noreffer">multi-engine execution</a>, and UDFs.</p>
<p>Here&rsquo;s an example of using DuckDB and Postgres in conjunction:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">xorq.api</span> <span class="k">as</span> <span class="nn">xo</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Connect to engines</span>
</span></span><span class="line"><span class="cl"><span class="n">pg</span> <span class="o">=</span> <span class="n">xo</span><span class="o">.</span><span class="n">postgres</span><span class="o">.</span><span class="n">connect_env</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">db</span> <span class="o">=</span> <span class="n">xo</span><span class="o">.</span><span class="n">duckdb</span><span class="o">.</span><span class="n">connect</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Load data from different sources</span>
</span></span><span class="line"><span class="cl"><span class="n">batting</span> <span class="o">=</span> <span class="n">pg</span><span class="o">.</span><span class="n">table</span><span class="p">(</span><span class="s2">&#34;batting&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">awards</span> <span class="o">=</span> <span class="n">xo</span><span class="o">.</span><span class="n">examples</span><span class="o">.</span><span class="n">awards_players</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="n">backend</span><span class="o">=</span><span class="n">db</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Filter in respective engines</span>
</span></span><span class="line"><span class="cl"><span class="n">recent</span> <span class="o">=</span> <span class="n">batting</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">batting</span><span class="o">.</span><span class="n">yearID</span> <span class="o">==</span> <span class="mi">2015</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">nl_awards</span> <span class="o">=</span> <span class="n">awards</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">awards</span><span class="o">.</span><span class="n">lgID</span> <span class="o">==</span> <span class="s2">&#34;NL&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Move data to postgres for join</span>
</span></span><span class="line"><span class="cl"><span class="n">result</span> <span class="o">=</span> <span class="n">recent</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">nl_awards</span><span class="o">.</span><span class="n">into_backend</span><span class="p">(</span><span class="n">pg</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="s2">&#34;playerID&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">result</span><span class="o">.</span><span class="n">execute</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><em>Move data between different engines within a single expression using <code>into_backend()</code>, here Postgres and DuckDB</em></p>
<p>You can see <strong>how easily you choose your most optimized execution engine</strong>, whether in the above example choosing DuckDB for filtering recent batting and using Postgres to filter NL (National League) awards, and joining the two with the Postgres engine.</p>
<p>Engines supported by xorq as of now, with the ability to move data between them, are (check <a href="https://docs.xorq.dev/api_reference/backends/supported_backends.html" target="_blank" rel="noopener noreffer">Supported backends</a> for the latest):</p>
<ul>
<li><strong>Embedded</strong>: DataFusion, DuckDB, SQLite, Pandas</li>
<li><strong>Warehouses</strong>: Snowflake, Databricks, Trino, Postgres</li>
<li><strong>Lakehouse</strong>: PyIceberg</li>
<li><strong>Arrow Flight</strong>: GizmoSQL (DuckDB over Arrow Flight SQL)</li>
</ul>
<h4 id="cross-engine-expression-tree">Cross-Engine Expression Tree</h4>
<p>With different engines supported, we can use the compressed single executable logic across engines. We can build <a href="https://docs.xorq.dev/getting_started/your_first_expression.html" target="_blank" rel="noopener noreffer">expression graphs</a> before executing them, which works like this, with <strong><a href="https://github.com/xorq-labs/xorq#one-expression-many-engines" target="_blank" rel="noopener noreffer">one expression, many engines</a></strong>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">expr</span> <span class="o">=</span> <span class="n">penguins</span><span class="o">.</span><span class="n">into_backend</span><span class="p">(</span><span class="n">xo</span><span class="o">.</span><span class="n">sqlite</span><span class="o">.</span><span class="n">connect</span><span class="p">())</span>
</span></span><span class="line"><span class="cl"><span class="n">expr</span><span class="o">.</span><span class="n">ls</span><span class="o">.</span><span class="n">backends</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The output of building a cross-engine expression is a <strong>directory containing your serialized pipeline</strong> with a unique hash identifying each build and its artifacts and expressions. When executed, the output is the resulting object or data.</p>
<p>And the <strong>expressions are tools, <a href="https://github.com/xorq-labs/xorq#expressions-are-tools-arrow-is-the-pipe" target="_blank" rel="noopener noreffer">Arrow is the pipe</a></strong>. E.g., a Unix pipe streams text between small programs. Xorq pipes Arrow streams between expressions:  <code>unix : programs :: xorq : arrow-transforms</code></p>
<p>That executes like this:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">In <span class="o">[</span>6<span class="o">]</span>: expr.to_pyarrow_batches<span class="o">()</span>
</span></span><span class="line"><span class="cl">Out<span class="o">[</span>6<span class="o">]</span>: &lt;pyarrow.lib.RecordBatchReader at 0x15dc3f570&gt;
</span></span></code></pre></td></tr></table>
</div>
</div><p>This is quite short and potentially abstract to understand when never used, but we will go into more examples and details in another article.</p>
<h2 id="a-shared-language-for-data">A Shared Language for Data</h2>
<p>This article introduces a new way of describing data transformations for machine learning or data engineering pipelines in a direct and simple way that works locally with any execution engine, without changing the code itself.</p>
<p>It&rsquo;s a good place if you <strong>need a trusted harness</strong> for a data engineering persona. We can define once and use it with the engine that works best for your workload and data engineering environment.</p>
<p>We had a look at how we <code>write -&gt; manifest -&gt; execute</code> with xorq, its advantages, and why you might use it for modeling once and representing everywhere. By adding AI agents to the mix, which help us pull the right lever, instead of bigger, more expensive models or more tokens, we improve accuracy with more semantic understanding, with a grammar the model can learn and apply, even pre-manifest before execution, and run them deterministically every time. This is a huge addition to working just with agentic Skills files that are free-form Markdown and pull data all over, or are not defined precisely enough. It&rsquo;s all about having high-quality context in the right format, with a clear definition where humans and AI agents can interchange and help each other.</p>
<p>There&rsquo;s a lot more to come, with showcasing the horizontal data stack and the use cases it supports, how we build expressions versus running computations, and how data catalogs are integrated into the picture, too.</p>
<p>&ndash;</p>
<p>Check out <a href="https://github.com/xorq-labs/xorq" target="_blank" rel="noopener noreffer">xorq code</a> and star it on GitHub, or read more behind the scenes at <a href="https://docs.xorq.dev/" target="_blank" rel="noopener noreffer">Xorq documentation</a>.</p>
<p>They also have a macOS desktop app coming up that does it all in one unified app, geared towards non-technical users. <a href="https://xorq.dev/" target="_blank" rel="noopener noreffer">Join the waitlist</a> for that.</p>
<hr>
<pre class=""><em>Full article published at <a href="https://xorq.dev/blog/grammar-for-data-engineering/" target="_blank" rel="noopener noreferrer">xorq.dev</a> - written as part of <a href="/services">my services</a></em></pre> 
]]></description>
</item>
<item>
    <title>Where AI Agents Belong in Data Engineering: The Correctness Layer</title>
    <link>https://www.ssp.sh/blog/where-agents-belong-in-de/</link>
    <pubDate>Tue, 07 Jul 2026 20:00:08 &#43;0200</pubDate>
    <author>Simon Späti</author>
    <guid>https://www.ssp.sh/blog/where-agents-belong-in-de/</guid><enclosure url="https://www.ssp.sh/blog/where-agents-belong-in-de/featured-image.webp" type="image/webp" length="0" /><description><![CDATA[<p>With ever-changing models, new and better ones coming out every few months, it&rsquo;s great if we don&rsquo;t have to rely on them too heavily. The better your tooling, the less dependent you become on any single model. That&rsquo;s also why the deterministic harness matters: a correctness layer that lets you reproduce outputs and trace lineage regardless of which model you&rsquo;re running underneath. This is especially true during maintenance or extending the project, where verification is the real job.</p>
<p>The danger isn&rsquo;t only a crash or an error message, but a wrong number that didn&rsquo;t break. It might be a clean query, but it introduces duplicated rows.</p>
<p>In this article, we go through the three levels of AI agents in data engineering, how to structure projects so the AI delivers its best outcomes, and how dedicated agents with a deterministic core help us build higher-quality pipelines — ones we can actually trust. And we look at a practical example of how it works with a blast radius analysis.</p>
<h2 id="the-three-levels-of-ai-agents-in-data-engineering">The Three Levels of AI Agents in Data Engineering</h2>
<p>Why should we use agents for data engineering? And at what levels can agents help us productively? As LLMs will always have some error tolerance, as humans do too, we need a way to be more confident in producing the code.</p>
<h3 id="chat-phase-autonomous-and-dedicated-tooling">Chat-phase, Autonomous and Dedicated Tooling</h3>
<p>There are different levels of confidence and levels on which the agents can help us.</p>
<ol>
<li>The initial <strong>chat-phase</strong>: the development where we prompt Claude or ChatGPT. The model tries to understand the context based on what it has access to. It takes a decent amount of tokens, as it needs to scan everything from scratch.</li>
<li>The <strong>autonomous approach</strong>, where Claude Code or Codex also have access to the tools humans have, mostly the CLI on the terminal, making it possible to query Postgres with psql or read from S3 or Parquet with DuckDB to verify queries and data. A much higher quality outcome.</li>
<li><strong>Dedicated agents</strong> for the task at hand. E.g., for data, the tools know dbt or know how to transpile SQL code deterministically, meaning not from training data only, but with an actual tool that does it much faster and more reliably. Built-in checks and features a &ldquo;general&rdquo; agent can&rsquo;t provide.</li>
</ol>













  

























<figure>
<a target="_blank" href="/blog/where-agents-belong-in-de/ai-agent-levels.png" title="Showcasing the three levels of AI agents in data engineering">

</a><figcaption class="image-caption">Showcasing the three levels of AI agents in data engineering</figcaption>
</figure>
<p>Ideally, we&rsquo;d want to always use the dedicated tools, but there isn&rsquo;t always one.</p>
<h3 id="where-in-the-de-lifecycle-each-level-actually-helps">Where in the DE Lifecycle Each Level Actually Helps</h3>
<p>BI Dashboards vs. Plumbing the Data Pipelines, or Creating Source Ingestions, or Maintaining? For data engineering, the question is not only if there is dedicated agent tooling, but also on what part of the <a href="https://www.oreilly.com/library/view/fundamentals-of-data/9781098108298/?ref=ssp.sh" target="_blank" rel="noopener noreffer">data engineering lifecycle</a> AI agents can help data engineers and analysts the most, and potentially even domain experts?</p>
<p>The lifecycle contains the ingestion part, ETL, or understanding the business in great detail, or is it just to visualize the result? Or should it cover maintenance in case of overnight ETL errors, or the full data lifecycle?</p>
<p>In general, before we go into more details later, agents can help us on the full cycle, but it always depends on who you are and what role you play. Building from scratch with <strong>no knowledge</strong> or <strong>seniority</strong> is dangerous. Why? Because they can&rsquo;t verify if the produced code is correct. Okay for a side project or a proof of concept, but not for actual production.</p>
<h3 id="whats-the-engineering-discipline-for-working-with-ai">What&rsquo;s the Engineering Discipline for Working with AI?</h3>
<p>There&rsquo;s also a part that is less technical, a way of guiding the agents in the right direction. Especially if we want to safely use it in large projects or organizations, we can&rsquo;t just let it run without guidance.</p>
<p>For that we need:</p>
<ol>
<li>clear <strong>project structure</strong> in which the agents can flourish. The more is given, the fewer tokens are used for this work, and it will be more aligned across the project. (Another reason a deterministic workflow such as <code>uv init</code> is best, because it will always be the same).</li>
<li>build with clear <strong>instructions</strong> (<a href="https://www.altimate.ai/skills" target="_blank" rel="noopener noreffer">agentic skills</a>, <a href="https://github.com/obra/superpowers" target="_blank" rel="noopener noreffer">superpowers</a>, etc.) on how the tools are used (basically providing CLIs and API documentation). This is the bulk of the work anyway. That&rsquo;s the data architecture, the brainstorming with fellow humans before you build something, instead of missing a key insight in the beginning and then letting the agent run down the wrong path. Also, be realistic: prompting &ldquo;be correct&rdquo; or &ldquo;use state-of-the-art&rdquo; won&rsquo;t make it more correct or more state-of-the-art than the model was trained on. So if it&rsquo;s a rather new architecture, it&rsquo;s a must that you provide these links and hints.</li>
<li><strong>set up</strong> the project in a modular fashion, so the agents cannot break the whole project if they make a small change, so you don&rsquo;t end up in a scenario like <a href="https://xkcd.com/2347/" target="_blank" rel="noopener noreffer">dependency hell</a> with everything dependent on each other.</li>
<li>use a <strong>declarative approach</strong>, with descriptive configuration that says the what and not the how, so that you can <strong>collaborate</strong> on these configs with the agents, version them, and easily revert or change something, as well as decouple the implementation logic from the actual business logic.</li>
</ol>
<p>With these steps, you can get the best out of the agents of today. I&rsquo;d say the model matters less, but the structure does, and as Mario says, so does the workflow approach. For example, extensively plan (the process before writing a single line) and correct the model before any implementation that could lead down the wrong path is written.</p>
<p>Also, don&rsquo;t overthink it. But this is only the workflow and learning the <strong>soft skills and discipline of working with agents</strong>. How does that look in a real-world project?</p>
<blockquote>
<p>[!note] The key is to get use out of AI, not to get more work.<br>
E.g., most developers used to think about the problem. Today, most drown in PRs. When the AI tooling gets better, AI can provide more quality code that is correct, that needs less review or fewer iterations, which means fewer PRs and less work for the developers to go through.</p>
</blockquote>
<h2 id="the-correctness-layer-for-data-engineers">The Correctness Layer for Data Engineers</h2>
<p>A key insight is that AI agents should support the &ldquo;human in the loop&rdquo; for <strong>correctness</strong>, or a <a href="https://blog.altimate.ai/the-correctness-layer-in-ade" target="_blank" rel="noopener noreffer">correctness layer</a>. And rather than making more work to verify more code, we should be confident in the process and know that the code it produces is verified and ultimately correct.</p>
<p>But how do we get more &ldquo;correct&rdquo; work and a layer in which we can verify it? The biggest argument is a deterministic-validation architecture in full. E.g., <a href="https://github.com/AltimateAI/altimate-code" target="_blank" rel="noopener noreffer">Altimate Code</a> splits the agent into a probabilistic layer on top and a deterministic Rust/TS layer underneath that does the actual SQL ops such as parsing, validating, and equivalence checks, so that the agent itself never has to be trusted on those questions.</p>













  

























<figure>
<a target="_blank" href="/blog/where-agents-belong-in-de/correctness-layer-de.png" title="An example of how Altimate Code is built with its probabilistic agent, deterministic harness, and deterministic core | Image from the article The Correctness Layer: Why Data Agents Need Determinism">

</a><figcaption class="image-caption">An example of how Altimate Code is built with its probabilistic agent, deterministic harness, and deterministic core | Image from the article <a href="https://blog.altimate.ai/the-correctness-layer-in-ade" target="_blank" rel="noopener noreffer">The Correctness Layer: Why Data Agents Need Determinism</a></figcaption>
</figure>
<p>Altimate Code, for example, is built on a probabilistic agent, deterministic harness, and deterministic core. The <strong>probabilistic agent</strong> with the LLM does the creative work of reading intent, picking a strategy, drafting SQL, summarizing results, and recovering when something goes wrong.</p>
<p>Below the boundary sits the <strong>deterministic harness</strong>, a TypeScript layer that intercepts every tool call: a dispatcher checks <code>hasNativeHandler</code> before the call runs, and routes it either to a native, deterministic handler or back to the model. Those handlers don&rsquo;t reimplement logic themselves, they call into the <strong>deterministic core</strong>, a Rust engine (<code>altimate-core</code>) that exposes SQL operations as pure functions over ASTs and schemas, wired in via napi-rs bindings. Parsing, validating, transpiling, checking query equivalence, diffing schemas, extracting column lineage, diffing rows across warehouses — all of it runs sub-millisecond, and all of it returns the same answer on the same input, every time.</p>
<p>Like a compiler, the agent never <em>decides</em> whether two queries are equivalent or a column exists upstream. Instead, it calls a function that proves it against the parsed AST and the schema, the same way a type-checker proves a program compiles rather than guessing.</p>













  

























<figure>
<a target="_blank" href="/blog/where-agents-belong-in-de/silent-wrong.png" title="How the correctness layer adds additional verification">

</a><figcaption class="image-caption">How the correctness layer adds additional verification</figcaption>
</figure>
<p>That&rsquo;s the distinction that makes the output easier to review, as factual checks have been run and the output is either correct, or there&rsquo;s a bug that it can fix directly. The rest a human can re-verify. On the dilemma of having stopped to hand-write code and approving it faster than humanly possible to check, you can also read more at <a href="https://blog.altimate.ai/you-are-the-trust-layer-managing-data-engineering-ai-agents-at-scale" target="_blank" rel="noopener noreffer">You Are the Trust Layer</a>.</p>
<blockquote>
<p>[!note] There&rsquo;s another factor, being wrong<br>
Bare agent use might be cheap, but only until they&rsquo;re wrong, and then the cost is unbounded.</p>
</blockquote>
<h3 id="improvements-for-better-usage-of-tokens">Improvements for Better Usage of Tokens</h3>
<p>Altimate, or data engineering agents that have deterministic functions and integrated understanding of how to work, can help you save tokens and be token lean (the opposite of [[tokenmaxxing]], which is popular on Twitter/X, using as many tokens as possible and having an agent running at all times). Because in large enterprises, token costs are a real budget point.</p>
<p>To slow down the tokens, an easy trick is to instruct the model to use fewer tokens and words itself - <a href="https://github.com/JuliusBrussee/caveman/" target="_blank" rel="noopener noreffer">caveman</a> is a good example of that, but you can also add a singular prompt to your <code>CLAUDE.md</code>, Codex, or model of choice in combination with Altimate Code.</p>













  

























<figure>
<a target="_blank" href="/blog/where-agents-belong-in-de/altimate-code-in-action.png" title="An example of Altimate Code showing a trace of data lineage and a web UI for it.">

</a><figcaption class="image-caption">An example of Altimate Code showing a trace of data lineage and a web UI for it.</figcaption>
</figure>
<p>There&rsquo;s a second, less obvious cost: the token itself isn&rsquo;t a stable unit. When Anthropic shipped Opus 4.7, the same prompt that cost X tokens on 4.6 <a href="https://blog.altimate.ai/the-great-token-heist-of-26" target="_blank" rel="noopener noreffer">started costing roughly 1.4X</a> (same input, same answer, more tokens, same price per token).</p>
<p>Altimate on <a href="https://blog.altimate.ai/the-great-token-heist-of-26" target="_blank" rel="noopener noreffer">The Great Token Heist of &lsquo;26</a> makes the case that &ldquo;cost-per-token is the wrong number to optimize&rdquo;, since the meter itself can move with a vendor&rsquo;s next model update, and what we should track instead is <strong>cost-per-task</strong>. I fully agree, and this is where deterministic function calls work around that volatility by not using a model/tokens for every task, making it less expensive.</p>
<h2 id="typical-use-cases">Typical Use Cases</h2>
<p>In this chapter we go through typical AI agent use cases for data engineering.</p>
<p>There are many of them. You can use them to educate yourself or your team, build production data pipelines, build data apps, and visualize your data in new innovative ways (usually HTML web pages with React and other JavaScript frameworks). But in general, the use cases fit into these approaches:</p>
<ol>
<li><strong>Start a new project from scratch example</strong>: Building a data landscape with more open source.</li>
<li><strong>Extending an existing project or data warehouse</strong>: Adding new data pipelines.</li>
<li><strong>Maintaining current setup</strong>: Update and verify it still works when changes come in.</li>
<li><strong>Migration</strong>: Migrate from one database or tooling to the next.</li>
<li><strong>Finding the Blind Spots</strong>: Two similar-sounding IDs might be wrongly used for a join, or missing data in a column that got missed in a nightly load, or anything in between. If agents can do these checks, that would be super beneficial. With more access to CLI, Model Context Layer, and deterministic tooling, these things are truly possible.</li>
</ol>
<p>Below we go through extending and changing an existing warehouse with a change of column, and using Altimate Code to give us a Blast-radius assessment.</p>
<h3 id="showcases-blast-radius-example">Showcases: Blast-Radius Example</h3>
<p>A <a href="https://en.wikipedia.org/wiki/Blast_radius" target="_blank" rel="noopener noreffer">Blast-radius</a> refers to the <strong>potential extent of damage</strong>. For example, before you knock down a wall in your house, you want to know if there&rsquo;s plumbing behind it, electrical wiring within it, or if it&rsquo;s holding up the floor above.</p>
<p>The same is true for a data warehouse or a data project with lots of ETL. For example, if a data engineer cleans up the table <code>fct_orders</code> by joining <code>orders</code> to <code>order_items</code> and summing <code>order_total</code>. It compiles, the dbt tests pass, nothing errors. But the join changes the grain, so any order with several line items now gets counted once per item, and revenue quietly inflates.</p>
<p>It&rsquo;s best to know, before you <strong>rename a column</strong> or add a new join, the downstream (data that comes after the current task) dependencies to the dashboard — that&rsquo;s what the blast-radius report does.</p>
<p>With Altimate Code we can achieve this. Before any change goes through, it maps out the full impact automatically and produces a detailed blast-radius report with what will break, what&rsquo;s safe, what needs someone to sign off, and also performs the changes. Here is what this looks like:</p>
<h4 id="rename-and-change-columns-and-logic">Rename and Change Columns and Logic</h4>
<p>As an example, in this prepared <a href="https://github.com/sspaeti/ecommerce_demos" target="_blank" rel="noopener noreffer">ecommerce repo</a> with different DWH layers such as <code>staging -&gt; intermediate -&gt; marts</code>, I prompted this request to change unit from cent to dollars:</p>
<p>






</p>
<p>It recognized the dbt name and invoked <code>dbt-analyze</code> automatically:<br>





</p>
<p>It gave me a full Blast-radius report and the impact my changes would have on the project:<br>





</p>
<p>Including semantics only, to point out what&rsquo;s safe and what&rsquo;s not:</p>
<p>




</p>
<p>With a fixed order to address breaking changes, semantics and docs, and intentionally untouched:<br>





</p>
<p>Notice, I hadn&rsquo;t said anything about blast analysis or using dbt-analyze — it did it on its own, ran dbt, and analyzed it deterministically.</p>
<p>This shows how <strong>Altimate Code looks behind the walls of data engineering</strong>, just like blast radius analysis.</p>
<p>If you want to see another example and a full blog post on Blast Radius, check out <a href="https://www.altimate.ai/blog/blast-radius-analysis-using-altimate-code" target="_blank" rel="noopener noreffer">Blast Radius Analysis Using Altimate Code</a>, and what Altimate Code did as in the <a href="https://www.youtube.com/watch?v=Npf7fHK43-k" target="_blank" rel="noopener noreffer">video</a>. Or Altimate provides many more examples and <a href="https://docs.altimate.sh/examples/" target="_blank" rel="noopener noreffer">Showcase</a> on their website such as <a href="https://www.youtube.com/watch?v=7MtD0NJjZS4" target="_blank" rel="noopener noreffer">Migrate SQL Server to Snowflake with dbt</a> or showing how to resolve <a href="https://docs.altimate.sh/examples/#an-upstream-schema-changed-what-just-broke" target="_blank" rel="noopener noreffer">An Upstream Schema Changed</a>.</p>
<blockquote>
<p>[!example] Connect a model to Altimate<br>
Make sure to connect to a model with <code>/connect</code> and choose an existing subscription with API credits, or any other subscription. I used <a href="https://opencode.ai/zen" target="_blank" rel="noopener noreffer">opencode zen</a> for my example, which includes e.g. Opus 4.8.</p>
</blockquote>
<h2 id="correctness-over-confidence">Correctness Over Confidence</h2>
<p>I hope you got a better understanding of why AI agents can be genuinely useful, especially when provided with the right tools and applied with the right discipline.</p>
<p>You&rsquo;ve also seen how deterministic tooling, purpose-built for data engineering and analytics problems, gets you both better correctness and better token economics than general-purpose agents alone.</p>
<p>Coming back to where we started: not every task needs a level-three agent. A quick chat-phase agent is fine for exploring a dataset or drafting a query you&rsquo;ll review yourself. But the moment that output touches production or serious work, a dashboard, a nightly job, a number someone makes a decision on, you want the deterministic core underneath it, not just a model that sounds confident.</p>
<p>That&rsquo;s the gap <a href="https://github.com/AltimateAI/altimate-code" target="_blank" rel="noopener noreffer">Altimate Code</a> is built to close. It runs on deterministic functions purpose-built for DE workloads, it&rsquo;s open-source via the OpenCode TUI, and for teams wanting more, there&rsquo;s Altimate Studio — a paid, multi-agent platform with extras like warehouse cost optimization, dbt development acceleration, and migration tooling.</p>
<p>&ndash;</p>
<p>Check out <a href="https://github.com/AltimateAI/altimate-code" target="_blank" rel="noopener noreffer">Altimate Code</a>, it&rsquo;s free and open-source. Give them a star if you like them, and find more information on their <a href="https://docs.altimate.sh/" target="_blank" rel="noopener noreffer">docs</a> and new <a href="https://www.altimate.ai/" target="_blank" rel="noopener noreffer">website</a>.</p>
<hr>
<pre class=""><em>Full article published at <a href="https://www.altimate.ai/blog/where-ai-agents-belong-in-data-engineering-the-correctness-layer" target="_blank" rel="noopener noreferrer">Altimate.ai</a> - written as part of <a href="/services">my services</a></em></pre>
]]></description>
</item>
<item>
    <title>The Process of Smart Note-Taking</title>
    <link>https://www.ssp.sh/blog/process-smart-note-taking/</link>
    <pubDate>Sun, 28 Jun 2026 00:40:17 &#43;0100</pubDate>
    <author>Simon Späti</author>
    <guid>https://www.ssp.sh/blog/process-smart-note-taking/</guid><enclosure url="https://www.ssp.sh/blog/process-smart-note-taking/featured-image.webp" type="image/webp" length="0" /><description><![CDATA[<p>I&rsquo;ve taken notes over my whole life. As a Swiss, I&rsquo;m very particular about organization and structured in filing my knowledge. I&rsquo;ve tried and taken notes in many different ways, from forgetting everything, to noting down every detail, to only taking listings of the most important things. Today, I can say, there&rsquo;s no right way, but I can see it&rsquo;s working for me, as my most interesting ideas grow inside my note taking system, and they power what I share online, and earning a living from it.</p>
<p>So what is important when taking notes? And how you might take notes too? First, the book [[Smart Note Taking]] by Sönke Ahrens helped me tremendously to understand the basics, that was after I was taking notes already for 10 years, but the principles of it being fine to delete notes, to go from literature notes to evergreen (though I changed that a bit for my liking), were two things that helped me a lot. To see the notes as a system of thoughts, more than just notes. From dumping all my thoughts into a blank file in my text editor of choice to sorting them, and getting clarity from it. And that <strong>writing is not the outcome of thinking, it is the medium in which thought occurs</strong>. This has grown since then, even more.</p>
<p>But if we think about it, we can also make it a hugely scientific and philosophical, but on the other hand, it&rsquo;s just ideas we have in our heads to:</p>
<ol>
<li>Write down in a way we don&rsquo;t forget, and more importantly</li>
<li>find them again in a year or two.</li>
<li>Also, do not write only a couple of words, as later you might not know exactly what you meant, but also don&rsquo;t write too many details, or worse, copy a full article.</li>
</ol>
<p>The key is to <strong>pause for a second</strong>, and think about it, and then write it down in a sentence or two. I usually use listings, so I know it&rsquo;s kind of a brainstorming, if I want to go deeper, I can just indefinitely indent and go up again. So I can, if I want, write a full outline, add new thoughts from the idea I just heard, link it to other ideas and notes in my vault.</p>
<p>But number two is harder than it looks to write it in a way to find it again easily. That took me a long time to optimize, and only changing of system from <a href="/blog/how-to-take-notes-in-2021/" rel="">OneNote to Obsidian</a>, and more so from file organization to connected notes, or also called [[Zettelkasten]]. This means, during the writing, I didn&rsquo;t need so much thought where to put the note, is it in business, or personal, should I create a new folder, or do I need to search all my notes if there&rsquo;s already a similar folder, to just open a new note, doesn&rsquo;t matter where it is, directly type, link it to at least one or two other notes (so I find it later if I forget the file name based on backlinks) and give it a name that I might search for in a year. That is crucial, as I almost exclusively search all my notes via file name, the name is key.</p>
<p>Luckily [[Obsidian]] helps us with duplicated names, because if you open double brackets anywhere with <code>[ [</code> and start typing, you find all notes with that name, so you don&rsquo;t accidentally create a note twice. And for the finding part, if I know I have written once about it, but I can&rsquo;t find the name, I search for the content with search or other mechanisms in Obsidian. But once I&rsquo;ve found it, I directly rename the note to something I was searching initially, as this means to me, the name was not good enough yet, or add a keyword. So next time I can find it immediately.</p>
<h2 id="the-process-of-note-taking">The Process of Note Taking</h2>
<p>There are different stages of note taking, the first part of capturing your note that we just described. But then there&rsquo;s the <strong>refinement</strong> and the <strong>publishing</strong> phase. Tiago Forte, for example, calls it CODE (Capture, Organize, Distill, Express).</p>
<p>While you can <strong>add a lot of work</strong> for yourself with telling yourself you need to refine your notes, take time every week to process the latest, I do none of that (scheduled or planned). I believe the note taking should help me and not add more work to me. Sure, I refine them, and a lot actually, but it&rsquo;s based on usage.</p>
<p>I look for signs. For example, I come back to a note over and over again, or I looked up a note two three times recently to understand a concept better, or I linked to a note many times. All of these are signs that this note needs more attention, potentially even splitting it, really refine, and then potentially publish on my public second brain (I can just add <code>#publish</code>, and it lands on my website), or write a full blown blog post on my blog website.</p>
<h2 id="the-key-of-format">The Key of Format</h2>
<p>One thing that unlocked so much more of the sharing part, and joy of using my notes, was the switch from the lock-in format in OneNote to an open [[Plain Text Files|Plaintext Files]] file [[Markdown]]. These files are just text files locally on my machine, meaning I can open them with my [[neovim|Text Editor]] or [[IDE]], or do a search and replace on all my files. Back them up easily, and also notice they almost use no space, as just text files. And the best part, I completely own them, so all my ideas and work I put into them, I will own forever as long as there will be computers. They will not die with the tool such as OneNote, Apple Notes or Notion, once they are gone, your notes are gone too, or if they deactivate your account, byebye all your notes.</p>
<h3 id="blogging">Blogging</h3>
<p>But the not foreseen effect, was that when I <a href="/brain/migrating-from-onenote-to-hugo/" rel="">migrated my blog from WordPress</a> ([[Rich Text]] interface) to a [[Static Site Generators (SSG)]] with [[GoHugo]], that is just Markdown. Now instead of needing to reformat, to upload somewhere in an online form, and constantly need to sync them between each other, say I updated my notes or fixed some grammar. No, now it&rsquo;s essentially the same note. Obviously, the blog post needs a title, a hero image, maybe a description and some more attributes, the plain note doesn&rsquo;t need.</p>
<p>But once I noticed this, I upgraded the workflow quite a bit. And I added my [[Quartz]] based second brain to my website (see <a href="/brain" rel="">ssp.sh/brain</a>), and there I had the goal to do zero edits, and just hit a button and publish. And to be honest, that&rsquo;s what I do for more than 6 years or so.</p>
<p>I add <code>#publish</code> to my note, and <a href="https://github.com/sspaeti/second-brain-public/blob/1889ae20d756b52c5c61f851838c0069973f3321/Makefile#L127" target="_blank" rel="noopener noreffer">run a script</a> (it ended up not being a button :), and it&rsquo;s on my website under new recent note. Including all the backlinks I added via Obsidian and formatting, links title and everything. I even added an [[Dynamic Feature Images|automatic hero image generator based on each note]], and updated a bunch of more advanced things with the frontmatter that Obsidian later added too, same as my GoHugo had already, meaning the format aligned even more, and is even more the same!</p>
<h2 id="what-ive-learned">What I&rsquo;ve Learned</h2>
<p>So what I&rsquo;ve learned? It&rsquo;s really important to remove friction, to make it so that you can use and enjoy it. Maybe for some it&rsquo;s needed the friction of opening a web form and typing in there, same as some like writing the email newsletter in an email client, to feel the pressure of the readers more, than just in a text editor - which I also implemented in [[neomd|my email client]] and newsletter in <a href="https://github.com/sspaeti/listmonk-rss/blob/main/newsletter.py" target="_blank" rel="noopener noreffer">newsletter-rss</a>, I can either run a script and start an email and upload it to my email newsletter, or write it to <code>newsletter-book@ssp.sh</code> and my email client will create a new campaign in my listmonk and send it out 30min later.</p>
<p>But that is what I was saying, the pressure of writing in the email or WordPress editor directly can be a help, but if you want to [[Learn in Public]] and share what you learned more often, I think removing friction is key. Also to overshare some times, when the note is still very rough. Because people might give you feedback, that helps your thinking or gives you an angle that you never had before.</p>
<h3 id="tldr">TL;DR</h3>
<ol>
<li>Take <strong>smart notes</strong></li>
<li>Use <strong>connected links</strong> instead of folders to break out of the 1 dimensional world of folders, especially if you write or want to learn. (it takes time though to adapt)</li>
<li>Choose an <strong>open format</strong>, I highly recommend Markdown. Also read [[File Over App]] as Kepano said, the app might not be here in 5 years, but your notes will.</li>
<li><strong>Publish notes to learn</strong>, make the process as smooth as possible, no formatting or copying/duplicating of your notes needed.</li>
<li><strong>Have fun, and enjoy the process</strong>. Especially with AI agents, choose a framework like GoHugo or Astro that builds on Markdown and plaintext files, and get the agent to help you with making the site pretty, adding features to the website that you like and make it truly your site. Not a boring site that everyone else has, not for others, but for you.</li>
</ol>
<p>This was a little bit of a ramble, but it&rsquo;s really a <strong>love letter</strong> to my 17 years of note taking. It [[My Note-Taking Path|evolved quite a bit]] sine I started, but stabilized now for quite a while with Markdown and open format with GoHugo.</p>
<p>If you are curious how the notes look, just go to <a href="/brain" rel="">my second brain</a> and check out my public notes and how all my notes are public and <strong>connected with an interactive graph and backlinks</strong>. You can literally dive into my brain, see what I am thinking about, see how all the notes connect, hopefulyl learn something new for yourself. If you do learn something new, please reach out and write me an email and tell me. I love connecting to like-minded people.</p>
<hr>
<p>Latest addition to my website here, I interconnected not only my second brain with the graph, but my blog and my book I&rsquo;m writing at dedp.online, on each chapter, blog or second brain note there is an interactive graph, and you can see links to the second brain and vice versa. Check this note as example, or see [[Future of Blogging]] where I elaborate a bit more on that topic, and included backlinks.</p>
<p>How did you evolve your blogging and note-taking over the years? Any other learnings? I also love the <a href="https://www.ssp.sh/blog/obsidian-note-taking-workflow/" target="_blank" rel="noopener noreffer">Obsidian Workflow</a> with templates I use everyday and the tools to write in with Obsidian, or check an older article on the same topic <a href="/blog/pkm-workflow-for-a-deeper-life/" rel="">Personal Knowledge Management Workflow for a Deeper Life</a>. Check out my <a href="/categories/productivity/" rel="">other blogs</a> if that interests you. I also share <a href="/blog/obsidian-note-taking-workflow/#how-i-create-initial-notes-templates" rel="">templates</a> in Obsidian, or to clip online articles with [[Obsidian Webclipper]] and its templates.</p>
<p>The whole second brain code is public at my <a href="https://github.com/sspaeti/second-brain-public" target="_blank" rel="noopener noreffer">second-brain-public</a> and how I automated the copying of my obsidian notes to my second brain, so I don&rsquo;t need to duplicate them, is described here [[Public Second Brain with Quartz]].</p>
]]></description>
</item>
<item>
    <title>Operationalizing Data Orchestration: Best Practices for DevOps, Infra, and Code Locations</title>
    <link>https://www.ssp.sh/blog/operationalizing-data-orchestration/</link>
    <pubDate>Sat, 27 Jun 2026 08:00:08 &#43;0200</pubDate>
    <author>Simon Späti</author>
    <guid>https://www.ssp.sh/blog/operationalizing-data-orchestration/</guid><enclosure url="https://www.ssp.sh/blog/operationalizing-data-orchestration/featured-image.webp" type="image/webp" length="0" /><description><![CDATA[<p>Operating the data orchestration layer can be as critical as the source OLTP databases, when decisions are taken from the data assets produced by downstream data pipelines. When orchestration gets at the heart of triggering and observing most of the schedules or assets, there is even more importance involved.</p>
<p>In <a href="/blog/dagster-almanack-open-data-platform" rel="">Part 1</a> I argued that the open data platform makes orchestration data and its flow simpler, providing integration into data assets and transformation, ETL, BI and even Kubernetes all while being open, making it a fully composable data stack. This piece continues with what it takes to operate orchestration, and looks at one of the hardest parts of data engineering &ldquo;DevOps&rdquo;, and how to manage different environments, business code vs infrastructure code, scalability and how orchestration.</p>
<p>I&rsquo;d argue that <a href="/brain/the-state-of-devops-in-data-engineering/" rel="">DevOps is what data engineering</a> was a decade ago, when everyone was talking about data science, but the actual work was 80% data engineering. With AI and generative AI, where even more data gets used for model training, or generated by AI, data engineering gets even more critical and larger, therefore also DevOps, which over a certain size gets the biggest task: deploying and upgrading to the latest error-free version, and keeping it stable and scalable.</p>
<h2 id="deployment-how-to-start-devops-vs-gitops">Deployment, how to Start: DevOps vs. GitOps:</h2>
<p>Before you start deploying or operationalizing anything, you will get into [[DevOps]] and more so, [[GitOps]]. That&rsquo;s why we start with that.</p>
<p>Because once you start with deploying your own infrastructure, you need a way to automate changes across your systems, or even your development server. You can&rsquo;t manually deploy on different systems. If you only have dev and production, it might work at the start, but there are so many dependencies, stateful NFS mounts where important configs or DWH data is stored, or dependencies between the installed tools from the BI tools to data integration and orchestration.</p>
<p>GitOps is taking that deployment git repo and including infra code such as Kubernetes deployments (also called <a href="https://en.wikipedia.org/wiki/Infrastructure_as_code" target="_blank" rel="noopener noreffer">Infrastructure as Code (IaC)</a>), and making them automatically deployable on git commit pushes to the git repo with post-trigger actions. With the goal of operationalizing data orchestration, we need to look at GitOps and DevOps best practices. GitOps is everywhere these days, with agents working best with Markdown or local YAML or code that is checked in to a git repo.</p>
<p>Orchestration is no different here, we want to have a repo for the <strong>code of data pipelines</strong> as well as <strong>infrastructure and ENVs for each environment</strong> (prod, test, dev). We need to separate infra from business-heavy ETL in the data pipeline. We want to standardize the deployment patterns (e.g. base and overlay is how Kustomize is doing it).</p>
<h2 id="how-to-operationalize-data-orchestration">How to Operationalize Data Orchestration?</h2>
<p>So now that we have GitOps ready and understand it, let&rsquo;s revise the challenges and then best practices of operationalizing a data orchestration tool. To make it more concrete, I use [[Dagster]] and its features whenever suited.</p>
<h3 id="the-challenges-of-deploying-data-orchestration">The Challenges of Deploying Data Orchestration</h3>
<p>One important part is how to deploy a system that will be at the heart of your data stack and responsible for the refreshing of your data assets.</p>
<p>The problems are multi-fold. Do you want to auto-scale with Kubernetes, Terraform, meaning you rent machines and scale up and down based on tasks in your data pipelines, spawning up multiple spark jobs, multiple DuckDB processes in parallel as part of spawned containers? Or do you want to pay for it and just connect data pipelines with Databricks Spark Cluster, and hook up Dagster+ that handles it all.</p>
<p>Compare it to the Titanic iceberg collision where too many holds overflowed and then it sunk. Alex Noonan <a href="https://youtu.be/r9SSpyp3N84?si=L5bBv6zFh5EUV8Fd&amp;t=241" target="_blank" rel="noopener noreffer">says</a> it&rsquo;s the same with data at scale, you get too many duplicated data pipelines, tools, inconsistent data contracts, unclear ownership and ungoverned sprawl.</p>
<blockquote>
<p>[!note] A handy feature built in: <strong>cost observability</strong> for cloud<br>
Dagster&rsquo;s ability to track compute costs per asset, per partition — this feeds directly into your &ldquo;open data platform&rdquo; narrative. FinOps for data pipelines.</p>
</blockquote>
<h3 id="best-practices-for-running-data-orchestration-organization-vs-deployment">Best Practices for Running Data Orchestration: Organization vs. Deployment</h3>
<p>The problems at scale can get complex and run into a lot of work, quite quickly. Therefore we need a clear setup that works for all our deployments. Here we look at separating the <strong>organization</strong>-side and the <strong>deployment</strong> itself.</p>













  
<figure><a target="_blank" href="/blog/operationalizing-data-orchestration/operationalizing-best-practices.webp" title="">

</a><figcaption class="image-caption">This shows the organizing part that is about the structure and the deployment handling state and business logic.</figcaption>
</figure>
<p>It all starts once you decide to run your own deployments and machines. Maybe you have a platform team or cloud server that you use with Kubernetes. That&rsquo;s when we need to separate the state vs stateless, and also the code of data pipelines is a key point here.</p>
<p><strong>1. Organize DevOps</strong> means to focus on <strong>structure and organization</strong>. We need the right organization for structure and repositories. For example, I once created a <a href="https://github.com/ssp-data/kubernetes-gitops-deployment-blueprint" target="_blank" rel="noopener noreffer">Kubernetes GitOps deployment Blueprint repo</a>, that includes such structural questions such as GitOps workflow patterns, infrastructure configs, tenant isolation, database migrations, and observability templates for production deployments with Flux.</p>
<p>The organization usually happens in the form of a git repository, and you have one for environment and deployment for infrastructure as code. You set up some sort of GitOps with Flux, ArgoCD, Terraform, or GitHub Actions or similar to automate deployment with a single <code>git commit &amp;&amp; git push</code>. All of the environments are typically handled with ENV variables.</p>
<p><strong>2. is Deployment</strong>, this is where we handle state and stateless. The deployment repo is usually mostly stateless. It has state for persistent storage locations for configs that Dagster, or BI tools or others need, but these are of a technical nature and usually can be recreated as there is no historic data stored as in a data warehouse where we ingest data from source OLTP databases for example, and version the changes. This way of not breaking state is much harder.</p>
<p>We need to make sure that the <strong>data assets produced by the data orchestrator stay intact</strong>, and don&rsquo;t get purged with a new deployment. Other things to take care of are technical resources such as the Spark integration as a resource Python file, which is stateless and can just be redeployed when we update the code, so we have the latest version prepared for the next run.</p>
<p>There&rsquo;s no complexity, and with proper versioning, we can also easily roll back by just setting the older version in our deployment repo, commit and push. GitOps will then make sure to roll back to a working version. And when the data itself needs repair, partitioned <a href="https://docs.dagster.io/guides/build/partitions-and-backfills/backfilling-data" target="_blank" rel="noopener noreffer">backfills</a> let you re-materialize a specific time range across the whole asset graph, with dependencies resolved automatically.</p>
<h4 id="code-locations-the-key-piece-to-connect-state-and-stateless">Code Locations: the Key Piece to Connect State and Stateless</h4>
<p>The key piece, again, is connecting state with no state, and Dagster offers an elegant solution here that I suggest for any data work: <strong><a href="https://dagster.io/blog/code-location-best-practices" target="_blank" rel="noopener noreffer">code locations</a></strong>  or also thought of <a href="https://kanton-bern.github.io/hellodata-be/concepts/workspaces/" target="_blank" rel="noopener noreffer">data engineering workspaces</a>. The code locations connect the state and stateless in a meaningful way. Code locations are both stateless (libraries used, technical implementations) but also stateful once the business logic has been run. The outputs are data assets used by downstream processes, and we can&rsquo;t just delete and recreate them easily. It might also be that some of the assets take hours to create, so just deleting them and then rerunning the full data warehouse for a day isn&rsquo;t desired.</p>
<p>Code locations are key to a successful orchestration deployment story where we differentiate business from technical implementation logic. Many start deploying Airflow or other data orchestrators and pretty quickly land at the questions: &ldquo;how do we update data pipelines?&rdquo;, &ldquo;do we give people access to the deployment repository?&rdquo;, &ldquo;where are all the envs for each environment?&rdquo;, &ldquo;how can business users quickly test their Python script or notebook?&rdquo;, &ldquo;what about access permissions to the data assets, are they allowed to see all tables, the full S3 bucket?&rdquo;.</p>
<p>With workspaces you can <strong>containerize your data pipeline</strong> logic, with its required Python libraries, into a docker image or similar, and test it and <strong>deploy independently from the deployment code</strong> that is usually only accessible to the platform team or data engineers if they do platform engineering. This leads to a <strong>natural separation</strong> between data wranglers and data platform people.</p>
<p>Here&rsquo;s an overview of how centralized teams can structure their code locations differently - from <strong>centralized to embedded and to hybrid</strong>, with dedicated workspaces for <strong>different teams</strong> such as data engineering, data scientists, finance or marketing:</p>













  
<figure><a target="_blank" href="/blog/operationalizing-data-orchestration/code-locations-teams.webp" title="">

</a><figcaption class="image-caption">Write -&gt; Manifest and Execute | Image from <a href="https://github.com/xorq-labs/xorq" target="_blank" rel="noopener noreffer">Composable expressions for data pipelines · GitHub</a></figcaption>
</figure>
<p>The differences are that centralized data team structures can give you consistency across teams, but are bottlenecked and slower when you want to change and iterate. Embedded means that teams have a link to each team, increasing speed and domain depth, but can lead to drift and fewer standards if not careful.</p>
<p>The different setup of teams is also something we have discussed since the dawn of data work. It started with a centralized data warehouse to a more shared Data Mesh and other approaches. With code locations, you can have a <strong>centralized data platform orchestrator</strong> while still having your domain ETL and data pipelines in dedicated team code repositories, using a hybrid with the best of both worlds.</p>
<p>The code locations also fit into the <a href="/blog/dagster-almanack-open-data-platform" rel="">right abstraction layer</a> as we talked about in the Dagster Almanack, meaning that code locations separate concerns for data teams, and with Dagster resources separate business from technical implementation, that can be used across different code locations (e.g. versioned PyPI packages), making the setup more flexible and adaptable to how data teams work.</p>
<h3 id="microservices-that-work-with-a-central-open-data-orchestrator">Microservices that Work with a Central Open Data Orchestrator</h3>
<p>The right abstractions with code locations let you use data pipelines <strong>as an advanced microservice</strong> with reusable technical code while using sole-purpose domain logic in each location, that integrate into the platform easily with Dagster abstractions and applied functional data engineering best practices. With something like <a href="https://docs.dagster.io/integrations/external-pipelines" target="_blank" rel="noopener noreffer">Dagster Pipes</a>, you can invoke external code — Spark jobs, notebooks, R or Rust processes — while logs and metadata still stream back into the asset graph.</p>
<p>If you need a better integration for less technically savvy people, you can use the <a href="https://dagster.io/blog/dsls-to-the-rescue" target="_blank" rel="noopener noreffer">in-built DSL</a>. DSL stands for Domain Specific Language, which lets you define your pipelines and transformations with a simple YAML interface. <a href="https://docs.dagster.io/guides/build/components" target="_blank" rel="noopener noreffer">Dagster Components</a> takes this further: a toolkit for packaging integrations and common patterns behind a YAML interface, so pipeline instances can be defined with little to no Python.</p>
<blockquote>
<p>[!note] General Learning from GitOps<br>
More learnings from using GitOps and deploying orchestration and general data workloads with Kubernetes, here are 8 more points:</p>
<ol>
<li><strong>Separation of concerns</strong> is crucial - keeping infrastructure, platform services, and business logic (pipelines) in distinct sections makes maintenance and collaboration easier.</li>
<li><strong>Standardized deployment patterns</strong> like the base/overlay structure with Kustomize allow for reusable configurations across environments with minimal environment-specific overrides.</li>
<li><strong>Versioned artifacts</strong> with timestamps (e.g., finance-pipeline-20250512123045.tar.gz) create a reliable release process that enables rollbacks and audit trails.</li>
<li><strong>Database migration</strong> automation tools, such as <a href="https://github.com/liquibase/liquibase" target="_blank" rel="noopener noreffer">Liquibase</a>, can handle schema changes programmatically across environments, thereby reducing manual errors.</li>
<li><strong>Test early, test often</strong> - validate data pipelines, infrastructure code, and database migrations separately before integration testing in an isolated environment.</li>
<li><strong>Workspaces separation</strong> from infrastructure code enables domain experts, such as data scientists and analysts, to focus on their core competencies while maintaining deployment standards.</li>
<li><strong>CI visibility</strong> through lineage diagrams and documented processes enables teams to understand the deployment flow and troubleshoot issues more efficiently.</li>
<li>GitOps, as the <strong>single source of truth</strong>, means changes occur through Git commits, creating an automatic audit trail and enabling pull request reviews.</li>
</ol>
<p>This overall blueprint, with best practices, helps you specifically with data engineering reference architecture for Kubernetes-based data platforms using GitOps workflow patterns. It includes infrastructure configs, tenant isolation, database migrations, and observability templates for production deployments with linked <strong><a href="https://github.com/ssp-data/gitops-flux-pipeline-showcase" target="_blank" rel="noopener noreffer">CI/CD implementation with GitOps</a></strong> infrastructure example.</p>
</blockquote>
<h2 id="how-to-deploy-dagster-kubernetes-helm-docker">How to Deploy Dagster: Kubernetes, Helm, Docker</h2>
<p>If we go one step deeper, we see different ways of deployment pretty quickly. We will keep this chapter short, as it would fill a full book, but generally you need to decide if you want to run scalable on something like Kubernetes, where pods and compute are automatically scaled up/down, or locally to test, with docker.</p>
<p>Or if you just want to get started without headache and lot of development time, this is where paying for something like <a href="https://dagster.io/lp/dagster-plus-trial" target="_blank" rel="noopener noreffer">Dagster+ service</a> makes sense.</p>
<p>The biggest question for data pipelines is <strong>where do you run the compute</strong>, do you scale up a Spark cluster, do you run Python or SQL engines as part of Kubernetes pods, or do you use any other way. Because that&rsquo;s where you will spend most money, as data pipelines are only expensive as the process they run, e.g. crunching large data sets with compute. Partitioned, incremental materialization is the other half of the cost equation. Processing only yesterday&rsquo;s partition instead of a full refresh is where the orchestrator saves compute money. The same scaling applies to <a href="https://docs.dagster.io/guides/test/asset-checks#partitioned-checks" target="_blank" rel="noopener noreffer">partitioned asset checks</a>, so quality validation only runs on the partition that changed.</p>
<p>You can think about it more high-level, so segregate <a href="https://www.thenile.dev/blog/storage-compute-separation" target="_blank" rel="noopener noreffer">storage from compute</a> with a Lakehouse or with tables and data stored in Open Table Formats (Iceberg, Delta and Hudi), that saves you money by not using expensive SSDs, but is slower and costs more on the analytics side when you query and join these data.</p>
<p>Here&rsquo;s a list that helps you get started to operationalize Dagster as the data orchestrator with different deployment options:</p>
<ol>
<li><strong>Helm Charts</strong> (recommended for production on Kubernetes): Official chart at <a href="https://github.com/dagster-io/dagster/tree/master/helm/dagster" target="_blank" rel="noopener noreffer">helm/dagster</a>: <code>helm repo add dagster https://dagster.io/helm</code>. Two charts: <code>dagster</code> (webserver + daemon infra) and <code>dagster-user-deployments</code> (your code locations), so pipeline code deploys independently of platform upgrades. Guide: <a href="https://docs.dagster.io/deployment/oss/deployment-options/kubernetes/deploying-to-kubernetes" target="_blank" rel="noopener noreffer">Deploying to Kubernetes</a>.</li>
<li><strong>Docker Compose</strong> (local or single VM, e.g. EC2): one container each for webserver, daemon, and per code location. Example: <a href="https://github.com/dagster-io/dagster/tree/master/examples/deploy_docker" target="_blank" rel="noopener noreffer">examples/deploy_docker</a>, guide: <a href="https://docs.dagster.io/deployment/oss/deployment-options/docker" target="_blank" rel="noopener noreffer">Docker deployment</a>. For pure local dev, just <code>dagster dev</code>.</li>
<li><strong>Kubernetes</strong> customization: there is no official operator — the Helm chart <em>is</em> the K8s story. Runs launch as K8s Jobs via <code>K8sRunLauncher</code>, with autoscaling configured and tunable per asset/job with <code>dagster-k8s/config</code> tags. See <a href="https://docs.dagster.io/deployment/oss/deployment-options/kubernetes/customizing-your-deployment" target="_blank" rel="noopener noreffer">Customizing your deployment</a>.</li>
<li><strong>AWS ECS / VM</strong> (if no K8s): Terraform/ECS pattern, example at <a href="https://github.com/dagster-io/dagster/tree/master/examples/deploy_ecs" target="_blank" rel="noopener noreffer">examples/deploy_ecs</a>, guide: <a href="https://docs.dagster.io/deployment/oss/deployment-options/aws" target="_blank" rel="noopener noreffer">Deploying to AWS</a>.</li>
<li><strong>Hosted: <a href="https://dagster.io/lp/dagster-plus-trial" target="_blank" rel="noopener noreffer">Dagster+</a></strong>: Serverless (they run everything) or Hybrid (control plane hosted, an <a href="https://docs.dagster.io/deployment/dagster-plus/hybrid/kubernetes/setup" target="_blank" rel="noopener noreffer">agent</a> runs compute in your K8s/ECS, deployed via the <code>dagster-cloud-agent</code> Helm chart).</li>
</ol>
<h2 id="guardrails-and-pitfalls">Guardrails and Pitfalls</h2>
<p>Beyond deploying and operationalizing the orchestrator itself, there are related topics that cut across data orchestration, some of which we touched on in the open data platform architecture image in <a href="https://dagster.io/blog/the-dagster-almanack-from-complexity-to-composability#why-open-data-platform-a-system-that-unifies-open-source" target="_blank" rel="noopener noreffer">Part 1</a>, where you see related tasks we need to deploy when wanting a full open data platform. The most important are testing and governance.</p>
<h3 id="governance--testing">Governance &amp; Testing</h3>
<p>A deployment is only as trustworthy as the data it produces. With <strong>integrated data quality checks</strong>, we enforce assertions on every run instead of testing data after the fact, something a BI tool can&rsquo;t do, and the source can&rsquo;t either, because the business transformation hasn&rsquo;t happened yet. The data pipeline is exactly where the <strong>business rules</strong> live, so it&rsquo;s where domain experts can enforce checks and data types they know must hold.</p>
<p>With <a href="https://docs.dagster.io/guides/test/asset-checks" target="_blank" rel="noopener noreffer">asset checks</a>, this even extends to assets external to your pipelines. Yes, writing checks is extra work, but it makes the whole system more stable and less complex: numbers are known to be correct, and you avoid the downstream ad-hoc fixes that persist as duplicated data marts or gold-layer (if we use <a href="https://www.databricks.com/blog/what-is-medallion-architecture" target="_blank" rel="noopener noreffer">Medallion Layers</a>).</p>
<p>Testing data isn&rsquo;t comparable to testing software, as data, tools, and volumes change between dev, test, and prod, and upstream from source data sources that we have no control over.</p>
<p>This is again where abstractions make life easier, e.g. Dagster separates business logic from technical implementation in resources. You can swap a mocked or local Postgres resource in dev for Snowflake in prod without touching pipeline code. Combined with the environment separation from our GitOps setup above, functional tests and balancing tests (does the count in the source match the warehouse?) can run in <a href="https://en.wikipedia.org/wiki/Continuous_integration" target="_blank" rel="noopener noreffer">Continuous integration (CI)</a> before a single <code>git push</code> ever reaches production.</p>
<p>Governance goes beyond quality checks: <strong>asset-level access controls</strong>, audit trails via metadata, column-level lineage for GDPR, and <a href="https://docs.dagster.io/examples/best-practices/pii-compute-logs" target="_blank" rel="noopener noreffer">PII redaction in compute logs</a> are the features that make a deployment enterprise-ready.</p>
<p>Obviously, testing and governance are their own domain and field, so that&rsquo;s worth another article, but I didn&rsquo;t want to leave it out, as it needs to be part of the strategy and best practices. For example, simply defining what your prefixes for table names in stage, or core, are, or how to name <code>last_updated</code> and <code>created_date</code> across the warehouse and deployment of your data platform or orchestration is key.</p>
<h3 id="provisioning-and-multi-tenancy">Provisioning and Multi-tenancy</h3>
<p>Data orchestration is what Dagster is made for, but <strong>provisioning</strong> is something that fits well in the scope of using Dagster and deployment. As a pipeline rarely runs alone, it also needs an object store, a data catalog, maybe a Spark cluster. Provisioning these accompanying services is itself orchestration, and platform teams increasingly use Dagster for exactly that. It is also an advanced, observable cron for infrastructure. In several organizations, this started with data pipeline deployment, and spread across the company from there.</p>
<p>So without provisioning the needed compute engines or related tooling for your data pipeline, operationalizing the data orchestrator and the full open data platform is not complete. This is also where in larger companies this task usually falls to two different teams, <strong>data practitioners</strong> (teams of data engineers, analysts, or power users) and the <strong>data platform team</strong>. The two teams are different, but they have the same need to orchestrate their many tasks and jobs:</p>
<table>
  <thead>
      <tr>
          <th>Data Practitioners: <strong>Autonomy</strong></th>
          <th>Data Platform Team: <strong>Leverage</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Focus on business logic in tool of choice</td>
          <td>Ensure practitioners are self-sufficient and productive</td>
      </tr>
      <tr>
          <td>Self-serve their pipelines in a productive SDLC</td>
          <td>Focus on high-impact, platform-wide improvements</td>
      </tr>
      <tr>
          <td>Work as close to their business stakeholders as possible</td>
          <td>Partner with cross-cutting policy teams (finance, compliance)</td>
      </tr>
      <tr>
          <td>Follow the lineage of their data assets to trust and debug them</td>
          <td>Provision shared services (object store, catalog, Spark) so lineage is observable in one place</td>
      </tr>
  </tbody>
</table>
<p>For enterprises, this pairs naturally with <strong>multi-tenancy and branch deployments</strong>: isolated environments that mirror production, so teams can review data pipeline changes like code PRs, and the GitOps workflow from the deployment chapter is applied to the data itself. A concrete example: Lakeflow Jobs can&rsquo;t express dependencies across Databricks workspace boundaries, but <a href="https://dagster.io/blog/how-to-orchestrate-across-multiple-databricks-workspaces-without-losing-your-mind" target="_blank" rel="noopener noreffer">bringing every workspace into a single asset graph</a> can.</p>
<h2 id="orchestration-as-the-operating-system-of-your-data-platform">Orchestration as the Operating System of Your Data Platform</h2>
<p>Closing the <a href="https://dagster.io/blog/the-dagster-almanack-from-complexity-to-composability" target="_blank" rel="noopener noreffer">initial Dagster Almanack</a> with its guidance, insights and tips for data platform engineers, as with this operationalizing data orchestration guide on DevOps, Infra, and Code Locations in the shared task of deploying a full open data platform.</p>
<p>We have seen how to operationalize data orchestration, and what features Dagster can bring to the table. The best practices are to organize and deploy around git repositories and with separation of concerns in mind. When you think about deployment, always ask yourself where your storage of data assets will be, and how to compute them. This will define the best strategy for finding the best platform for deployment.</p>
<p>The <a href="https://dagster.io/blog/the-dagster-almanack-from-complexity-to-composability" target="_blank" rel="noopener noreffer">open data platform</a> deployment also helps <strong>escape the modern data stack trap</strong>. Dagster helps you integrate different tools in line with deployment strategies while separating workspaces for different teams and domain experts in an elegant way that works well with GitOps strategies. Exchanging compute engines and using the best tools for the task is just one config change away, and doesn&rsquo;t need to change the logic of the data pipeline itself.</p>
<p>With newer features such as state-backend components, Dagster uses these for integrations whose definitions depend on external metadata. Instead of re-querying external systems every time a code location loads, Dagster can fetch that state at controlled times, persist it, and then build definitions from the cached result.</p>
<p>It makes it a <strong>composable toolkit for data engineers</strong> out of the box and a single pane of glass for the data platform, incrementally adopted across teams and stakeholders. Tools are integrated into a single data platform, with a single deployment strategy, where multiple teams work together.</p>
<p>In the future with more shifting left, DataOps that combines this all under one domain, and with AI agents that govern and help create data pipelines, this foundational data platform is more critical than ever, integrating and scheduling all tasks in a structured and reliable way.</p>
<hr>
<pre class=""><em>Full article published at <a href="https://dagster.io/blog/the-dagster-almanack-operationalizing-data-orchestration" target="_blank" rel="noopener noreferrer">Dagster.io</a> - written as part of <a href="/services">my services</a></em></pre>
]]></description>
</item>
<item>
    <title>Vibe Coding Is Dangerous, Agentic Engineering Isn&#39;t—Wes McKinney</title>
    <link>https://www.ssp.sh/blog/how-to-use-ai-with-de-wes-mckinney/</link>
    <pubDate>Thu, 11 Jun 2026 09:40:08 &#43;0200</pubDate>
    <author>Simon Späti</author>
    <guid>https://www.ssp.sh/blog/how-to-use-ai-with-de-wes-mckinney/</guid><enclosure url="https://www.ssp.sh/blog/how-to-use-ai-with-de-wes-mckinney/featured-image.png" type="image/png" length="0" /><description><![CDATA[<p>This series interviews real practitioners to extract the patterns behind how they actually use AI in their data work today. This is the second interview in &lsquo;How to use AI with DE&rsquo;, and this time we have none other than Wes McKinney.</p>
<p>Creator of Pandas, probably the most widely used data analysis library for Python, Wes has shaped the era of data and is co-creator of Apache Arrow. He also created Ibis to address these issues with a different approach to Python dataframe libraries, by decoupling the dataframe API from the backend implementation.</p>
<p>The article is structured in four parts: <strong>(1)</strong> how to trust the outcome, <strong>(2)</strong> knowing what not to build, factoring in cost-per-token among others, <strong>(3)</strong> accountability of agents and the code they generate, and <strong>(4)</strong> philosophizing about the future of agentic engineering.</p>
<h2 id="introducing-the-guest-3-wes-mckinney">Introducing the Guest: #3 Wes McKinney</h2>
<p>Besides creating the most popular dataframe libraries used by most data people, Wes McKinney now focuses full time on agentic engineering with his newly founded company <a href="https://kenn.io/" target="_blank" rel="noopener noreffer">Kenn Software</a>, which focuses on the promise of building a new stack of development and knowledge systems for the agentic era. He&rsquo;s also doing AI and Python at <a href="https://posit.co/" target="_blank" rel="noopener noreffer">Posit</a>, where they work on a <a href="https://positron.posit.co/" target="_blank" rel="noopener noreffer">data science IDE</a>. He&rsquo;s a part-time <a href="https://composed.vc/" target="_blank" rel="noopener noreffer">investor</a> in various startups.</p>
<p>Wes has been running Claude Code, Codex, and Gemini CLI for months. Thousands of sessions, hundreds of thousands of messages. He has released multiple tools that help the agentic work (more on this later), and he is at the forefront of what&rsquo;s going on with his recent blog posts about &ldquo;<a href="https://wesmckinney.com/blog/agent-ergonomics/" target="_blank" rel="noopener noreffer">Why he uses programming languages built for agents, not humans</a>&rdquo; and <a href="https://wesmckinney.com/blog/mythical-agent-month/" target="_blank" rel="noopener noreffer">Mythical Agent Month</a>, with his recent insights into how to work with agents. Find all his takes at <a href="https://wesmckinney.com/" target="_blank" rel="noopener noreffer">Wes McKinney.com</a>.</p>
<p>I had the pleasure of asking Wes more about these topics, and we&rsquo;ll go into more details, plus many other things. Let&rsquo;s get started.</p>
<h2 id="how-to-trust-the-outcome">How to Trust the Outcome?</h2>
<p>We started the interview with a critical question that stands above all others in the current AI landscape, and I asked him: &ldquo;<strong>Can we trust the outcome?</strong>&rdquo;. What if we need something important, other than a hobby project? What if the data <strong>must be correct</strong> (hospitals, banks)?</p>
<p>Similar to what Mark Freeman told us in our <a href="/blog/specs-over-vibes-interview-mark-freeman/" rel="">last interview</a> about using spec-driven development with <a href="https://github.com/github/spec-kit" target="_blank" rel="noopener noreffer">spec-kit</a>, Wes uses a similar approach, but with an agentic skill framework called <a href="https://github.com/obra/superpowers" target="_blank" rel="noopener noreffer">superpowers</a> (currently 216k stars on GitHub). Compared to spec-kit, it specs out the requirements differently by (A) <strong>guiding you through the conversation</strong>, asking you the right questions to get to what you want to build, and (B) once you fire it off, it spawns a sub-agent that keeps the implementing agent on track. Wes said, &ldquo;<em><strong>Superpowers looks for drift</strong></em>&rdquo;, and course-corrects if the implementing agents drift off to non-relevant, or not even specified, tasks.</p>
<p>Wes spends a lot of time in this specification phase, sometimes hours, very detail-oriented and engaged. Even before he starts speccing, he has subconsciously worked over the topic and idea for a long while. He will not start implementing something when he doesn&rsquo;t know super clearly how it fits together. The insights, the architecture, come from him. But the interview style by superpowers helps him <strong>clarify his thinking</strong>.</p>
<p>He doesn&rsquo;t only give his feedback to the questions, but sometimes also fires up multiple agents and integrates their feedback. Codex models especially seem to work well for design questions.</p>
<p>He puts a lot of importance on the spec being:</p>
<ol>
<li><strong>Spec conformant</strong>: Meaning the agents act in accordance with your specific set of rules, standards, or specifications.</li>
<li><strong>Code correctness and quality</strong>: This is where Wes uses e.g. <a href="https://github.com/kenn-io/roborev" target="_blank" rel="noopener noreffer">Roborev</a>, his own created AI-reviewer.</li>
</ol>
<p>Correctness is crucial, which led to creating Roborev. Wes developed many tools that help him work agentically, and we&rsquo;ll hear about many more later. Roborev, for example, is a code reviewer that can be initialized with a hook on a git repository, and from that moment on, every commit will be auto-reviewed by Codex (the default, but you can choose others too).</p>
<p>I use Roborev myself, and this is what the interactive TUI looks like - showing the most recently fired hooks with their running status, but most importantly, whether the review passed (<code>P</code>) or failed (<code>F</code>):<br>







</p>
<p>




</p>
<p>If it failed, you can open the review and see detailed findings categorized into severity <code>low, medium and high</code>:<br>





</p>
<p>The convenient workflow is that you copy the review with <code>y</code> and feed it back to your running agent to let it fix things directly. The current agent that created the change works best, as it already has all the context, compared to starting a new one that needs to load context and what has been done.</p>
<p>Roborev also helps to <strong>review a smaller part at a time</strong>. Wes also says it will <strong>never catch all the errors, but LLMs are very good at pattern matching</strong>, which is what error finding is, and they find many that might be missed. On top, he adds reviewers with different roles, e.g. <strong>giving agents roles</strong> such as focusing on security, CI, software development, or performance, which gives much more accurate feedback than a general reviewer.</p>
<p>After having gone through the spec intensively, having made sure that drift happens as little as possible, and having auto-reviewed each commit by Roborev, what is <strong>left for him to review is much less now, and of high quality</strong>. He then reviews the code and checks that it looks and does what he expects or envisioned.</p>
<p>Wes has a very clear problem or idea that he then solves meticulously. However, at the same time, he runs agents in parallel and works on many projects concurrently, context-switching between them<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<blockquote>
<p>[!note] Rigorous process in place needed: Changing models</p>
<p>The rigorous process he does is also needed because the models are constantly changing and are very unpredictable. It is hard to have a consistent outcome if you do not have reviewers and processes in place. And Wes says these AI reviewers are much better than just static analysis.</p>
</blockquote>
<h3 id="how-to-maintain-agentic-or-general-projects-over-time">How to Maintain Agentic, or General Projects over Time?</h3>
<p>The second question was about maintaining projects and how Wes <strong>handles maintenance</strong>, as <strong>creating projects is usually the easy part</strong>, but maintaining them for years to come is difficult. And how does he see that in combination with AI? Will that be outsourced to AI?</p>
<p>First of all, Wes uses his own projects and tools. That&rsquo;s the reason they exist, and it helps him find bugs. This is why he fixes errors or bugs when he runs into them. Besides Roborev, which helps tremendously to review and have fewer errors while developing, he uses <a href="https://github.com/kenn-io/middleman" target="_blank" rel="noopener noreffer">Middleman</a> to keep an eye on his agents and projects. It&rsquo;s another tool he built that gives him a local-first GitHub dashboard and triages what to maintain or fix from other users.</p>
<p>He automated repetitive work such as releasing with a full release script so he can release fast and fix bugs fast. The Changelog on GitHub is fully streamlined, too. He is also careful about what comes into the main branch, only changes he has verified and assessed as &ldquo;pass&rdquo;.</p>
<p>To illustrate what Wes is maintaining, here are some of the projects Wes built recently, some of which he might not have built without AI:</p>
<ul>
<li><strong><a href="https://github.com/roborev-dev/roborev" target="_blank" rel="noopener noreffer">roborev</a></strong>: Continuous code review for AI coding agents. Runs in the background and surfaces issues per commit before they compound.</li>
<li><strong><a href="https://github.com/wesm/middleman" target="_blank" rel="noopener noreffer">middleman</a></strong>: Local-first GitHub dashboard for maintainers to triage, review, and merge PRs and issues across repos.</li>
<li><strong><a href="https://github.com/wesm/agentsview" target="_blank" rel="noopener noreffer">agentsview</a></strong>: Local coding agent session viewer for Claude, Codex, and Gemini with analytics and full-text search.</li>
<li><strong><a href="https://github.com/wesm/msgvault" target="_blank" rel="noopener noreffer">msgvault</a></strong>: Archive a lifetime of email and chat locally, with full Gmail backup, DuckDB analytics, a TUI, and an MCP server for AI queries.</li>
<li><strong><a href="https://github.com/wesm/moneyflow" target="_blank" rel="noopener noreffer">moneyflow</a></strong>: Personal finance data interface for power users, supporting backends like Monarch Money and YNAB.</li>
<li><strong><a href="https://github.com/wesm/spicytakes.org" target="_blank" rel="noopener noreffer">Spicy Takes</a></strong>: LLM-analyzed blog posts from 20+ prolific tech writers, each with a TL;DR, key quotes, and a spiciness rating.</li>
<li><strong><a href="https://github.com/wesm/vibepulse" target="_blank" rel="noopener noreffer">VibePulse</a></strong>: Simple macOS menubar app to monitor Claude Code and Codex token consumption.</li>
<li><strong><a href="https://github.com/wesm/kata" target="_blank" rel="noopener noreffer">kata</a></strong>: Local-first issue tracker for AI-assisted software work, with an agent-friendly CLI and human-facing TUI.</li>
</ul>
<blockquote>
<p>[!note] Earlier Tools and Frameworks Wes Has Built</p>
<ul>
<li><strong><a href="https://github.com/posit-dev/positron" target="_blank" rel="noopener noreffer">Positron</a></strong>: A next-generation data science IDE built on VS Code, supporting Python and R.</li>
<li><strong><a href="https://github.com/pandas-dev/pandas" target="_blank" rel="noopener noreffer">pandas</a></strong>: The most widely used data analysis library in Python.</li>
<li><strong><a href="https://github.com/apache/arrow" target="_blank" rel="noopener noreffer">Apache Arrow</a></strong>: Language-independent columnar memory format for analytics.</li>
<li><strong><a href="https://github.com/ibis-project/ibis" target="_blank" rel="noopener noreffer">Ibis</a></strong>: Portable Python dataframe API that works across any backend.</li>
</ul>
</blockquote>
<h3 id="building-for-maintainability-modular">Building for Maintainability: Modular?</h3>
<p>I asked him if he builds for better maintainability, e.g. builds in a modular way so the AI agents can easily fix something or create a feature in a dedicated area without breaking the full program.</p>
<p>He didn&rsquo;t answer the modularity part directly, but Wes implements and uses tests extensively. If something needs to exist, he writes a test for it. But even more, by investing in test infrastructure, <strong>regression tests</strong> help prevent bugs and protect existing features during rapid development.</p>
<p>He also mentions that <strong>bugs are created faster these days, but also fixed faster</strong>.</p>
<h2 id="how-to-decide-what-to-build-saying-no">How to Decide what to Build? Saying No!</h2>
<p>Given that AI can get addictive, and in a time when you can build almost anything, I asked Wes how he knows what to build, and when to say no to avoid building the &ldquo;wrong things&rdquo;.</p>
<p>He said that:</p>
<blockquote>
<p><em>It&rsquo;s not the ideas on their own, he&rsquo;s thinking a lot about what he wants to build.</em></p>
</blockquote>
<p>Again, it is in his subconscious. He thinks and asks himself all day: &ldquo;How is it beneficial for agents? For humans? How can it be applied?&rdquo;</p>
<p>If he can&rsquo;t explain it, he will think more. For example, <a href="https://github.com/kenn-io/msgvault" target="_blank" rel="noopener noreffer">msgvault</a> didn&rsquo;t have a web interface, and he could have easily added one from the very beginning, but he didn&rsquo;t have a clear picture. So he just postponed it until later, when he had a use case, a pain point, or a real need.</p>
<blockquote>'<em>Those are the constraints</em>', Wes adds. '<strong><em>Because if you don't, AI will bring in lots of crap</em></strong>'.</blockquote>
<p>Superpowers also helps him with <strong>guardrails by keeping the AI on track</strong>. Besides, Wes has a perfectionist mindset, making him want to perfect the tool that works for him and improve the workflow.</p>
<h3 id="when-he-was-building-without-ai-pandas">When He Was Building without AI: Pandas</h3>
<p>It was the same when he was building Pandas: he was building it for his use case when fiddling with Excel. Then <strong>there is taste</strong>.</p>
<blockquote>
<p><em>Every prompt, every decision in the spec phase adds up to 100s or 1000s of small decisions, essentially <strong>manifesting one&rsquo;s taste</strong>. That&rsquo;s why the product comes out differently from two people, even though they use the same LLM models.</em></p>
</blockquote>
<blockquote>
<p>[!note] Find more at AI Council Talk about the Scope, Design and Taste in the Mythical Agent Month</p>
<p>Wes gave a very insightful talk at <a href="https://www.youtube.com/playlist?list=PLAesBe-zAQmFX-daAAnl6eeXhFHNU3Oms" target="_blank" rel="noopener noreffer">AI Council 2026</a> about this very topic, called the <a href="https://www.slideshare.net/slideshow/the-mythical-agent-month-ai-council-2026-talk-by-wes-mckinney/287532329" target="_blank" rel="noopener noreffer">Mythical Agent month</a>. He said what is left is &ldquo;<strong>Scope, Design and Taste</strong>&rdquo; with <strong>Conceptual Integrity</strong> (from the book by Fred Brooks).</p>
</blockquote>
<h3 id="saying-no-is-our-last-defense">Saying No is Our Last Defense</h3>
<p>In his recent <a href="https://www.slideshare.net/slideshow/the-mythical-agent-month-ai-council-2026-talk-by-wes-mckinney/287532329" target="_blank" rel="noopener noreffer">slides</a>, he shares &ldquo;<em>When code is free, saying no is our last defense</em>&rdquo;:</p>
<blockquote>
<p><em>Every new feature is cheap to create but expensive to maintain. Each one adds surface areas for bugs, confusion, and future agent mistakes.</em></p>
</blockquote>
<blockquote>
<p>[!tip] &ldquo;Hell Yeah or No&rdquo;: A similar term by Derek Sivers<br>
Similar to Wes&rsquo;s figuring out and saying no as our last defense, Derek Sivers said something similar before, where you say no to everything until you feel &ldquo;Hell Yeah&rdquo;. This Hell Yeah or No approach doesn&rsquo;t seem to have changed much with AI. This doesn&rsquo;t apply only to AI, but also to life and career, in my opinion.</p>
</blockquote>
<h3 id="cost-per-token-at-true-price-will-stop-the-waste">Cost-per-Token at True Price Will Stop the Waste</h3>
<p>A very current topic is how the growing <strong>cost-per-token</strong> factors into this decision of what to build. Or does it not? There&rsquo;s even a term called <a href="https://en.wikipedia.org/wiki/Token_maxxing" target="_blank" rel="noopener noreffer">token maxxing</a> that encourages programmers to use more tokens, whether by the company or by peer pressure on X/Twitter.</p>
<p>Wes was at the top of the <a href="https://tkmx.odio.dev/" target="_blank" rel="noopener noreffer">HN leaderboard</a> at some point, currently on <code>#4</code>:<br>





</p>
<p>Wes&rsquo;s current usage is <code>~$20,000/month</code> at API rates, which he sees on another tool he built called <a href="https://github.com/kenn-io/agentsview" target="_blank" rel="noopener noreffer">AgentsView</a>. He said that</p>
<blockquote>
<p><em>He thinks that all his high-quality output through the shared tools or the <strong>work he does is higher than the invested money</strong>.</em></p>
</blockquote>
<p>But on the economics side, he thinks that:</p>
<blockquote>
<p><em>Subscriptions go away, and <strong>pay by usage, a good thing</strong>. AI slop and low-value projects go away. This helps pay the <strong>true cost of tokens</strong>, which isn&rsquo;t the case for now, making the consumption (or even waste) of lots of tokens non-problematic.</em></p>
</blockquote>
<h4 id="enterprise-token-per-employee-clarify-useful-vs-vanity-ai-work">Enterprise Token per Employee: Clarify Useful vs. Vanity AI Work</h4>
<p>This was actually one reason why he built AgentsView: to have an overview of your own usage, a better &ldquo;token intelligence&rdquo;, but also at a larger company to measure each developer&rsquo;s usage. It could be part of performance reviews, showing each user&rsquo;s <strong>token spend vs the value generated</strong>.</p>
<p>You&rsquo;d have to justify your tokens, the opposite of now, where developers at Meta or Amazon are expected to burn tokens without incentives. <strong>Right now, it&rsquo;s the wild-wild-west</strong> (something previous interview guest Chris Riccomini <a href="/blog/how-to-use-ai-with-de-chris-riccomini/" rel="">also said</a>).</p>
<h2 id="accountability-of-agent-generated-code-who-is-responsible">Accountability of Agent-generated Code? Who is Responsible?</h2>
<p>My next question was how do we make people accountable for things they didn&rsquo;t create (<a href="https://en.wikipedia.org/wiki/Vibe_coding" target="_blank" rel="noopener noreffer">vibe coded</a>)? I gave the example of self-driving cars: who takes accountability if a Tesla hurts someone? (That&rsquo;s one reason full self-driving is still not allowed in Europe, as it&rsquo;s legally not settled who is accountable.)</p>
<h3 id="vibe-coding--vibe-coding-but-agentic-engineering">Vibe Coding ≠ Vibe Coding: But Agentic Engineering</h3>
<p>Wes made clear that what he does is not vibe coding, but <strong>agentic engineering</strong>. All the planning and architecting with superpowers and his newly created tools is not the same as vibe coding.</p>
<p>The term vibe coding to him means when you just one-prompt it, don&rsquo;t look at the code, and ship it. Again, this is not what he does.</p>
<p>He says:</p>
<blockquote>
<p><em><strong>We can&rsquo;t disengage from planning and writing specs</strong>. We can move much faster, but don&rsquo;t vibe code. <strong>Vibe coding is very dangerous and irresponsible</strong>.</em></p>
</blockquote>
<p>Like the <a href="https://x.com/brian_armstrong/status/2051616759145185723" target="_blank" rel="noopener noreffer">Coinbase example</a>, he finds letting non-technical employees push to production highly dangerous. We humans, with fundamental understanding and seniority, need to be more engaged in designing and testing, as coding is essentially &ldquo;cheap&rdquo; now.</p>
<p>He continues:</p>
<blockquote>
<p><em>Automated code review certainly helps, but it isn&rsquo;t a substitute for engineering experience.</em></p>
</blockquote>
<h2 id="philosophize-about-the-future-with-agentic-engineering">Philosophize about the Future with Agentic Engineering</h2>
<p>Wes is also an investor, a person who foresees the landscape well with his involvement in major data libraries. I asked him: &ldquo;If you think about AI, where would you invest your money? What do you trust will have the most benefit or will work well with AI?&rdquo;</p>
<p>Where do you see the <strong>future heading</strong>, or where does this end? Especially when we talk about data engineering?</p>
<h3 id="future-of-data-engineering">Future of Data Engineering</h3>
<p>He says that he is not involved too much in data engineering anymore, but that he is an investor in <a href="https://composed.vc/" target="_blank" rel="noopener noreffer">dlt, MotherDuck, and Bruin</a>. But his main focus is on <strong>agentic work</strong>, somewhat on top of the &ldquo;dbt legacy&rdquo;<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</p>
<p>But what he sees as currently the hot topic is <a href="https://cube.dev/blog/headless-bi" target="_blank" rel="noopener noreffer">Headless BI</a>, custom dashboards, and building a <a href="/blog/semantic-layer-duckdb/" rel="">semantic layer</a> for better context for agents. Things like business rules and sending the &ldquo;right&rdquo; queries. Building new <strong>knowledge systems</strong> for companies. For example, through msgvault, which <strong>extracts value from years of emails</strong> and easily makes them searchable.</p>
<p>He saw people building personal CRMs on top of msgvault and their emails. That&rsquo;s the current direction we are heading, he says.</p>
<h3 id="how-do-we-still-learn-by-learning-by-osmosis">How Do We Still Learn? By Learning by Osmosis</h3>
<p>The challenge will be: how do we develop senior engineers without writing code anymore? Wes himself doesn&rsquo;t write much code anymore, but reviews, guides, and adds taste. I asked him how someone can gain the work experience he has without the coding or going through the pain of coding, while avoiding the danger of not learning anything new, or getting overwhelmed with constant stimulation and potentially becoming addicted.</p>
<p>He says the hard labour goes away, which is where we usually learn. This is the way of <strong>learning by osmosis</strong><sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>, where we acquire knowledge while failing or naturally through exposure and immersion. He thinks the <strong>focus needs to be on design patterns and understanding architecture</strong>, to have the technical vocabulary to guide or understand the agents.</p>
<h2 id="next-interview">Next Interview</h2>
<p>I hope you enjoyed this interview number 3 with Wes. Huge thanks to Wes for taking the time to speak with me and for sharing his experience with all of us. Follow him on <a href="" rel="">Website</a>, <a href="" rel="">LinkedIn</a>, <a href="" rel="">X/Twitter</a>, or on <a href="" rel="">Bluesky</a>, and follow along on his new company <a href="https://kenn.io/" target="_blank" rel="noopener noreffer">Kenn Software</a>, or check out his agentic engineered tools he built at <a href="https://github.com/kenn-io" target="_blank" rel="noopener noreffer">GitHub</a>.</p>
<p>There is one more interview already lined up with none other than Maxime Beauchemin, so please share feedback, questions you might want to ask, or just your experience on how to work with AI in the data space. We&rsquo;re all in this together, figuring it all out. The more we can learn from each other, what&rsquo;s important, and maybe also what&rsquo;s not, the better.</p>
<hr>
<pre class=""><em>Full article published at <a href="https://motherduck.com/blog/vibe-coding-dangerous-agentic-engineering-wes-mckinney/" target="_blank" rel="noopener noreferrer">MotherDuck.com</a> - written as part of <a href="/services">my services</a></em></pre>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>On the podcast with Joe Reis, Wes <a href="https://www.youtube.com/watch?v=uC6g8L8zquE" target="_blank" rel="noopener noreffer">shared</a> that he was very locked-in, always had running agents, building things, which was &ldquo;<a href="https://wesmckinney.com/blog/mythical-agent-month/" target="_blank" rel="noopener noreffer">terrible for his sleep schedule</a>&rdquo;, but very fun.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>dbt as the incumbent that predates AI&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>&ldquo;Learning by osmosis&rdquo; is an idiomatic expression drawing on the figurative sense of <em>osmosis</em>: the gradual, often unconscious absorption of knowledge through exposure rather than deliberate study. <a href="https://www.collinsdictionary.com/dictionary/english/osmosis" target="_blank" rel="noopener noreffer">Collins English Dictionary</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
</item>
<item>
    <title>Beyond the Semantic Layer: Building a Context Layer for the Agentic Era</title>
    <link>https://www.ssp.sh/blog/agentic-context-layer/</link>
    <pubDate>Thu, 11 Jun 2026 08:00:08 &#43;0200</pubDate>
    <author>Simon Späti</author>
    <guid>https://www.ssp.sh/blog/agentic-context-layer/</guid><enclosure url="https://www.ssp.sh/blog/agentic-context-layer/featured-image.png" type="image/png" length="0" /><description><![CDATA[<p>Writing SQL was never the hard part. Making it <em>accurate and trustworthy</em> against your warehouse always was. Point an AI agent like Claude or Codex at your data stack and ask a real analytics question, and the answer is usually mediocre: the agent can scrape some context from your git repos or whatever metadata it can find, but it doesn&rsquo;t know your joins, your metric definitions, or the business rules that give a number its actual meaning.</p>
<p>So how do we make data agents reliable and accurate for database queries? Everyone talks about harnesses, evals, and context layers, but the real challenge is bringing them together with data engineering and the context you already have, such as database schemas, a semantic layer, metric definitions, plus the business knowledge that normally never reaches the agent.</p>
<p>That&rsquo;s the question this blog tackles: how agents can work with the data stack and analytics, and how a context layer fits in. We also take an inside look at <strong>ktx</strong>, a new context layer that reads from the usual sources but also the less obvious ones (Markdown, Notion, etc.), driven by agentic workers.</p>
<p>The idea is to pull two kinds of knowledge into one reviewable place: the hard semantics (your warehouse schema, joins, and metric definitions as YAML and SQL) and the soft semantics (the business context living in docs, wikis, and Notion that agents usually never see). Both are committed to git and reviewed like code, so a human stays in the loop while agents get a <em>warm start</em> instead of a cold database connection. The payoff: more accurate answers with fewer (and cheaper) queries against the warehouse.</p>
<p>SOURCESWarehouseschema · metricsBI toolsdashboards · joinsNotion + wikisoft semanticsDocs + Markdownnotes · contextCONTEXT LAYERAuto-built. Reviewed in git.Hard semanticsYAML + SQL the warehouse runsSoft semanticsMarkdown the team readsreviewed like codeAI agentaccurate SQL · governed</p>
<p>A context layer ingests your warehouse, BI tools, and docs into hard and soft semantics, then serves governed SQL to your agent.</p>
<h2 id="modeling-with-analytics-ai-agents-with-a-context-layer">Modeling with Analytics AI Agents, with a Context Layer</h2>
<p>Every business or data analyst faces mediocre results when prompting Claude or Codex on their data stack. It might figure out some context by reading the git repos or other metadata it can find. Still, the hard part is teaching the internals and business context that are unique to each business domain and company. So, how do we make AI agents for data and <strong>analytics reliable and accurate for database queries</strong>? Do we need to manually copy and paste Google Docs and Markdown files into the prompts, or can a context layer provide a more reliable, safe, and governed way to ask an AI assistant for analytics?</p>
<p>These days, it&rsquo;s much easier to ingest or add almost any number of new data sources with custom-built ETL data pipelines, whereas before we had to make hard decisions about what to include and what not. An AI agent, such as Claude or Codex, can merge multiple data pipelines that access the source database via CLIs and destinations via MCP, API, or CLI. But we still need an API and a process for updating source data, not just once. We need to make sure, test, and verify that the data is correct, potentially more than ever.</p>
<p>The challenge remains in modeling the data in a way that represents what the business is, making sure data flows fast but is also correct. But any AI assistant is only as good as the context we give it, and how easily it can read and express context, metrics, models, and knowledge. So, does the context layer solve these problems?</p>
<p>The context layer primarily supports the <strong>accuracy of SQL queries, continuous updates to the business context, and governance</strong>. Additionally, with a newer context layer, we can include more relevant business insights that are stored internally, usually in unstructured form, in tools like Notion as business documentation. Traditionally, these data weren&rsquo;t included because they weren&rsquo;t a &ldquo;pure data source&rdquo;, but they can be helpful for decision-making, especially when done in an agentic way. Every bit of <strong>human-written data can help the agent make better decisions</strong>. So the ultimate problem we solve is to include more useful data and have a more agentic engineered way that is faster and more streamlined, especially to start with.</p>
<h3 id="whats-an-agentic-context-layer">What&rsquo;s an Agentic Context Layer?</h3>
<p>The core is to turn warehouse metadata, BI tool definitions, query history, docs, and approved metric definitions into reviewable files that agents can search and execute. We feed valuable metadata and actual data to agents and humans so that they can be ingested and discovered more easily in one place.</p>
<p>It&rsquo;s a further evolution of a [[semantic layer]], such as Cube or AtScale, which are more focused on the modeling of metrics and domain knowledge and usually have less extensive knowledge of documentation, metadata outside of metrics, joins, and source data schema. And building a semantic layer from scratch can take time, as we need to extract metrics and unify them in a single repository in an additional layer. The main driver for a context layer is that it&rsquo;s automatically generated and includes <strong>additional business knowledge</strong> that lives outside the technical containers of data engineers, such as data catalogs, DDLs, or YAMLs, in tools like Confluence, Notion, and internal wikis.</p>
<p>I have written about how to model data in the agentic era, <a href="https://www.rilldata.com/blog/data-modeling-for-the-agentic-era-semantics-speed-and-stewardship" target="_blank" rel="noopener noreffer">Data Modeling for the Agentic Era (Semantics, Speed, and Stewardship)</a>, and we&rsquo;ve seen how metrics and context can strengthen a BI tool or insights. The context layer is all about context that we already have from our database catalogs (information_schema, data types, tables), more technical metadata, but also from our metrics inside the BI tools or in a declarative YAML, while adding new sources such as documentation in Markdown, rich text, fetched via API, CLI, etc. All with the goal of giving agents and LLMs more information to make the right calls autonomously.</p>
<p>Maxime Beauchemin calls documentation, wikis, and Markdown soft semantics, whereas YAML and the SQL metrics are hard facts or hard semantics. More soft semantics, such as business context from domain experts, help agents, and we need to make them available to other agents and humans too, building a <strong>full context layer</strong> that grows fast, where <strong>governance is essential</strong>.</p>
<h3 id="enhancing-gen-ai-trustworthiness">Enhancing Gen AI Trustworthiness</h3>
<p>Google describes <a href="https://cloud.google.com/blog/products/business-intelligence/how-lookers-semantic-layer-enhances-gen-ai-trustworthiness" target="_blank" rel="noopener noreffer">How Looker’s semantic layer enhances gen AI trustworthiness</a>, and a semantic layer <em>can</em> reduce errors for generative AI and autonomous agents creating queries. They say:</p>
<blockquote>
<p>With the <strong>foundation of a semantic layer, rather than asking an LLM</strong> to write SQL code against raw tables with ambiguous field names (e.g., <code>order.sales_sku_price_US</code>), the LLM is empowered to do what it excels at: searching through clearly defined business objects within LookML (e.g., <code>Orders &gt; Total Revenue</code>). These objects can <strong>include metadata and human-friendly descriptions</strong> (e.g., &ldquo;The sum of transaction amounts or total sales price&rdquo;).</p>
</blockquote>
<p>We also acknowledge that data modeling for agents is not the same as data modeling for humans, and that shapes how we work with agents and context together.</p>













  

























<figure>
<a target="_blank" href="/blog/agentic-context-layer/kaelio-modeling.png" title="Modeling for agents vs. modeling for humans: an agent only knows what the context makes explicit.">

</a><figcaption class="image-caption">Modeling for agents vs. modeling for humans: an agent only knows what the context makes explicit.</figcaption>
</figure>
<h3 id="context-layers-saving-literal-money">Context Layers Saving Literal Money</h3>
<p>In the end, it&rsquo;s also saving money, as more governance means the agent knows the system in more detail and therefore needs to make fewer queries to the actual database, or can do so in fewer iterations.</p>
<p>Queries on Snowflake, BigQuery, and Fabric can be expensive, and agents work at a different speed than humans. If we add more context to the agents, they need fewer queries because the information can be extracted from the retrieved context.</p>
<p>The agents have access to all this data, either directly or through CLI, skills, or MCP. The big advantage is that the agents get a warm-up and do not start with a cold database connection. All the warehouse schema, business semantics, BI usage patterns, and human documentation are <strong>in one single place</strong>.</p>
<p>Also, on the other end, verifying that the queries are correct is hard. More context can help with this.</p>
<h2 id="entering-ktx-open-source-context-layer">Entering <code>ktx</code>: Open-source Context Layer</h2>
<p>Kaelio launches its new open-source context layer solution called <a href="https://github.com/Kaelio/ktx" target="_blank" rel="noopener noreffer">ktx</a>. The goal is to get an OSS context layer solution that everyone can use for the future of analytics.</p>
<p>It started as an AI data agent platform, with the agent performing well on accuracy benchmarks against other platforms, which helped make the case for developing it further. Dashboards and agent interfaces are easier to build than ever, but these data apps need more durable and complete knowledge from all the company&rsquo;s data. This extended data added with ktx makes the agent <strong>more accurate</strong>.</p>
<blockquote>
<p>[!info] Want to get started immediately?<br>
You can quickstart with a demo project that fetches data from Postgres and Notion, with a dbt project and a Metabase interface. See <a href="https://www.kaelio.com/start" target="_blank" rel="noopener noreffer">Quick Start with public demo data</a>, or check out the <a href="https://github.com/Kaelio/ktx" target="_blank" rel="noopener noreffer">GitHub repo</a> for the full code. If you already have a project with relevant context and sources, get started with a simple prompt: <code>Run npx skills add Kaelio/ktx --skill ktx and use the ktx skill to install and configure ktx</code>. More at the <a href="https://docs.kaelio.com/ktx/docs/getting-started/quickstart" target="_blank" rel="noopener noreffer">Quickstart docs</a>.</p>
</blockquote>
<h3 id="how-ingesting-business-context-works">How Ingesting Business Context Works</h3>
<p>So how do we enhance business context for agents to make use of it?</p>
<p>Ingestion happens through two parallel items:</p>
<ul>
<li><strong>ingests and prepares</strong> both data context such as data models, historical queries, BI dashboards, etc. and</li>
<li><strong>business context</strong> such as internal docs, Slack, etc., in a format that data agents can use to operate more reliably and with stronger governance.</li>
</ul>
<p>If we look at a high-level overview, compared to a common semantic layer or logical layers in BI tools, you can build a <strong>knowledge layer</strong> around your business, with not only structured data but also unstructured knowledge from your company-wide internal documentation on Notion, or your data in Markdown or a git repository. ktx helps navigate and maintain this business knowledge.</p>
<p>Both of these can substantially enhance your &ldquo;context&rdquo;, basically critical business insights that are usually internal or in a format that can&rsquo;t be easily used. The challenge here is obviously to separate the updated and accurate documentation or knowledge from the outdated, which needs more focus or rigid process flow/rules on documentation to keep the latest updates, adding an owner and also going back to docs and invalidating them as no longer relevant to <strong>improve the context quality</strong>.</p>
<p>






<br>
<em>A knowledge layer wraps structured data and unstructured company knowledge into one place agents can use. <a href="https://www.youtube.com/watch?v=5V4TuzYVlrA" target="_blank" rel="noopener noreffer">See the full video</a>.</em></p>
<p>Smart agents such as Claude, Codex or any other that get context have built-in tools to understand business metrics, and combined with this newly ingested unstructured knowledge, they can help us get accurate responses against our warehouse or across our various data sources, and act as the human curator of data sets and entities for BI dashboards and apps.</p>
<p>






<br>
<em>Agents pair their built-in reasoning with ingested unstructured knowledge to answer questions across your sources. <a href="https://www.youtube.com/watch?v=5V4TuzYVlrA" target="_blank" rel="noopener noreffer">See the full video</a>.</em></p>
<p>If you compare using raw Claude Code vs. the ktx semantic layer component<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> with extended context, you get faster and more correct answers with ktx. Here&rsquo;s such an example:</p>













  
<figure><a target="_blank" href="/blog/agentic-context-layer/ktx-semantic-layer-knowledge-wiki-3.webp" title="">

</a><figcaption class="image-caption">Raw Claude Code vs. ktx with extended context: faster and more correct answers. <a href="https://www.youtube.com/watch?v=5V4TuzYVlrA" target="_blank" rel="noopener noreffer">See the full video</a></figcaption>
</figure>
<p>ktx has basically two connected sides where the first one <strong>builds and maintains</strong> the context layer, and the second <strong>serves that context</strong> to agents at runtime, which you see above.</p>
<h3 id="anatomy-of-context-layer-in-ktx">Anatomy of Context Layer in ktx</h3>
<p>The anatomy of a context layer is based on two files and two jobs. <strong>YAML</strong> for what the warehouse can execute. <strong>Markdown</strong> for what the team needs to interpret it. Both are committed to git and reviewed like code.</p>













  
<figure><a target="_blank" href="/blog/agentic-context-layer/ktx-semantic-layer-knowledge-wiki-4.webp" title="">

</a><figcaption class="image-caption">Anatomy of a context layer, from the <a href="https://docs.kaelio.com/ktx/docs/concepts/the-context-layer#a-navigable-graph" target="_blank" rel="noopener noreffer">ktx docs</a></figcaption>
</figure>
<p>The wiki is the self-organizing collection of ingested Markdown files. And for the YAML files, ktx has a similarly self-organizing executable semantic layer.</p>
<p><code>ktx ingest</code> pulls raw data from your data stack into a context layer, where you can also edit and create additional context manually. Additionally, you plug ktx into your agent of choice, which allows that agent to send extra memories for ingestion to keep the context up to date based on actual source data.</p>
<p>The semantic part is a set of functions that define metrics precisely, like &ldquo;monthly revenue&rdquo; or &ldquo;profit&rdquo;, so if you ask your agents, ktx knows exactly how to pull the data and answer the query with SQL, running it consistently.</p>
<h2 id="context-as-code-how-to-use-context-layer">Context as Code: How to Use Context Layer</h2>
<p>ktx is open source and you can install it with <code>npm install -g @kaelio/ktx</code>. It has a <strong>self-improving context layer</strong> that teaches agents how to query your warehouse accurately, from approved metric definitions, joinable columns, and business knowledge it builds and maintains for you.</p>
<p>With the ktx CLI you get <strong>context as code</strong>, writing wiki pages and semantic-layer definitions as git-based files you can review, diff, and merge. This is to update and enhance current context (documentation) you have in your company, and provide it to your agents as well. Git-based, so humans and agents can collaborate on it by editing the same context.</p>
<p>So how does it work? Here are the most common commands you can run and use. We don&rsquo;t go through the whole <a href="https://docs.kaelio.com/ktx/docs/cli-reference/ktx" target="_blank" rel="noopener noreffer">CLI reference</a>, but cover the most important ones to get a feel for how to work with the <code>ktx</code> CLI. Six commands cover the loop from <code>check status → discover → verify → serve</code>.</p>
<blockquote>
<p>[!note] Setup: Demo data<br>
This assumes you have set up ktx and want to work with it. If you want to follow along, or build up your own, check the <a href="https://docs.kaelio.com/ktx/docs/getting-started/quickstart" target="_blank" rel="noopener noreffer">quickstart with public data</a> Kaelio provides.</p>
</blockquote>
<h3 id="1-is-everything-wired-up--ktx-status">1. Is Everything Wired Up? → <code>ktx status</code></h3>
<p>First, get the status, which tells you which connections, LLM, and embeddings are live before you do anything else.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ktx status
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">  LLM           claude-code · sonnet  ✓
</span></span><span class="line"><span class="cl">  Embeddings    all-MiniLM-L6-v2 (384d)  ✓
</span></span><span class="line"><span class="cl">  Storage       sqlite (state) · sqlite-fts5 (search)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  Connections (4)
</span></span><span class="line"><span class="cl">    ✓ orbit      postgres
</span></span><span class="line"><span class="cl">    ✓ metabase   metabase
</span></span><span class="line"><span class="cl">    ✓ dbt        dbt
</span></span><span class="line"><span class="cl">    ✓ notion     notion
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="2-discover--search-metrics--ktx-sl">2. Discover &amp; Search Metrics → <code>ktx sl</code></h3>
<p><code>sl</code> is the <strong>semantic layer</strong>, the YAML definitions that tell agents how to turn &ldquo;revenue&rdquo; into correct SQL. Bare <code>ktx sl</code> lists every source and you can add a query to search. Add <code>--json</code> when an agent (not a human) is reading.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ktx sl                       <span class="c1"># list all semantic sources</span>
</span></span><span class="line"><span class="cl">ktx sl <span class="s2">&#34;revenue&#34;</span> --json      <span class="c1"># search, machine-readable</span>
</span></span><span class="line"><span class="cl">ktx sl <span class="s2">&#34;int_active_contract_arr&#34;</span> --json
</span></span></code></pre></td></tr></table>
</div>
</div><p>The JSON result is rich: a relevance <code>score</code>, a <code>snippet</code>, and <code>matchReasons</code> showing <em>why</em> it matched (lexical, token, semantic). Trimmed:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;int_active_contract_arr&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;path&#34;</span><span class="p">:</span> <span class="s2">&#34;semantic-layer/orbit/_schema/orbit_analytics.yaml#int_active_contract_arr&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;columnCount&#34;</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;score&#34;</span><span class="p">:</span> <span class="mf">0.036</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;matchReasons&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;lexical&#34;</span><span class="p">,</span> <span class="s2">&#34;token&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This is what makes an agent <em>cheaper and more accurate</em> as it finds the right metric from context instead of guessing field names against your warehouse.</p>
<h3 id="3-verify-before-you-trust--validate--query---format-sql">3. Verify Before You Trust → <code>validate</code> + <code>query --format sql</code></h3>
<p>When you edit a metric, always test first against the live schema with the following command:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ktx sl validate mart_revenue_daily --connection-id orbit
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>validate</code> catches missing columns, bad joins, and unsafe source names, so you fix the YAML <em>before</em> it hits production data. Then compile the SQL and <strong>read it</strong> before running anything:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ktx sl query <span class="se">\
</span></span></span><span class="line"><span class="cl">  --connection-id orbit <span class="se">\
</span></span></span><span class="line"><span class="cl">  --measure mart_revenue_daily.total_net_revenue <span class="se">\
</span></span></span><span class="line"><span class="cl">  --dimension mart_revenue_daily.revenue_date <span class="se">\
</span></span></span><span class="line"><span class="cl">  --format sql
</span></span></code></pre></td></tr></table>
</div>
</div><p>Swap <code>--format sql</code> → <code>--execute --max-rows 100</code> once the SQL looks right. The pattern is always: <em>compile, eyeball, then run.</em></p>
<h3 id="4-search-the-soft-semantics--ktx-wiki">4. Search the Soft Semantics → <code>ktx wiki</code></h3>
<p>Metrics are the <em>hard</em> facts. The wiki is the <strong>soft semantics</strong> we were talking about, such as the business definitions, rules, and gotchas in Markdown that agents search for context. Same shape as <code>sl</code>: bare lists, query searches.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ktx wiki <span class="s2">&#34;new hire&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">  GLOBAL (4 pages)
</span></span><span class="line"><span class="cl">    #1  new-hire-onboarding-requirements  — what every new hire must know by week one
</span></span><span class="line"><span class="cl">    #2  orbit-arr-methodology             — contract-first ARR definition + NRR treatment
</span></span><span class="line"><span class="cl">    #3  orbit-company-overview            — what Orbit sells, plan tiers, workflow
</span></span><span class="line"><span class="cl">    #4  implementation-handoff-process    — Sales Ops → CS handoff requirements
</span></span></code></pre></td></tr></table>
</div>
</div><p>When <a href="https://docs.kaelio.com/ktx/docs/cli-reference/ktx-wiki#options" target="_blank" rel="noopener noreffer"><code>sqlite-fts5</code></a> is configured, wiki search is <strong>hybrid</strong> with lexical, token, and semantic (embeddings) lanes combined and ranked together. That&rsquo;s how an agent finds &ldquo;ARR methodology&rdquo; when someone asks about &ldquo;annual revenue.&rdquo;</p>
<h3 id="5-the-raw-escape-hatch--ktx-sql">5. The Raw Escape Hatch → <code>ktx sql</code></h3>
<p>Need to peek at the actual rows? <code>ktx sql</code> runs <strong>read-only</strong> SQL (single <code>SELECT</code>/<code>WITH</code> only) against a connection.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ktx sql --connection orbit <span class="s2">&#34;select count(*) from orbit_analytics.int_procurement_qualifying_actions&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div class="details admonition note open">
        <div class="details-summary admonition-title "><i class="icon admonition-icon icon-note"></i>Two gotchas worth knowing up front<i class="details-icon  admonition-icon admonition-icon-arrow-right"></i></div>
        <div class="details-content">
            <div class="admonition-content"><ol>
<li>Not every connection executes SQL. A <code>metabase</code> connection has no native scan connector. Supported drivers are <code>sqlite</code>, <code>postgres</code>, <code>mysql</code>, <code>clickhouse</code>, <code>sqlserver</code>, <code>bigquery</code>, and <code>snowflake</code>.</li>
<li>The table name must be real. <code>relation &quot;public.orders&quot; does not exist</code> usually means you&rsquo;re guessing the schema, which is exactly the problem the semantic layer (<code>ktx sl</code>) exists to solve.</li>
</ol>
</div>
        </div>
    </div>
<p>Most of the time you want <code>ktx sl query</code>, not raw <code>ktx sql</code>. Reach for <code>sql</code> only when you genuinely need rows the semantic layer doesn&rsquo;t model yet.</p>
<h3 id="6-hand-it-to-your-ai-agent--ktx-mcp--ktx-setup">6. Hand it to Your AI Agent → <code>ktx mcp</code> + <code>ktx setup</code></h3>
<p>This is what your business users or engineers might use most often, connected with actual agents. Start the MCP server, then wire up your agent of choice:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ktx mcp start
</span></span><span class="line"><span class="cl">ktx setup --agents --target claude-code
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>setup</code> drops a <code>.mcp.json</code> into your project and installs the analytics skill. Next time Claude Code opens in that directory, it picks up the <code>ktx</code> MCP server and starts answering questions <em>with your governed context</em>, calling <code>ktx sl</code> and <code>ktx wiki</code> under the hood instead of hallucinating field names.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">● Skill(ktx-analytics)
</span></span><span class="line"><span class="cl">  Called ktx 8 times
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">● Here are the metrics defined in the orbit semantic layer:
</span></span><span class="line"><span class="cl">  mart_arr_daily         arr                  ARR ($) — contract-first
</span></span><span class="line"><span class="cl">  mart_revenue_daily     total_net_revenue    gross − credits − refunds
</span></span><span class="line"><span class="cl">  mart_customer_health   at_risk_customer_count   distinct high-risk accounts
</span></span><span class="line"><span class="cl">  ...
</span></span></code></pre></td></tr></table>
</div>
</div><p>The agent discovered every metric on its own. No copy-pasted schema, no guesswork.</p>
<h3 id="the-whole-loop">The Whole Loop</h3>
<p>Recapping the full workflow in one loop:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ktx status              <span class="c1"># 1. is it wired up?</span>
</span></span><span class="line"><span class="cl">ktx sl <span class="s2">&#34;revenue&#34;</span>        <span class="c1"># 2. discover metrics</span>
</span></span><span class="line"><span class="cl">ktx sl validate …       <span class="c1"># 3. verify against schema</span>
</span></span><span class="line"><span class="cl">ktx wiki <span class="s2">&#34;ARR&#34;</span>          <span class="c1"># 4. pull in business context</span>
</span></span><span class="line"><span class="cl">ktx mcp start           <span class="c1"># 5. serve it to your agent</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>When you&rsquo;re done, shut the runtime down cleanly:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ktx admin runtime stop
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>Go deeper:</strong> <a href="https://docs.kaelio.com/ktx/docs/guides/building-context" target="_blank" rel="noopener noreffer">Building Context</a> · <a href="https://docs.kaelio.com/ktx/docs/guides/writing-context" target="_blank" rel="noopener noreffer">Writing Context</a> · <a href="https://docs.kaelio.com/ktx/docs/guides/reviewing-context" target="_blank" rel="noopener noreffer">Reviewing Context</a> · <a href="https://docs.kaelio.com/ktx/docs/guides/serving-agents" target="_blank" rel="noopener noreffer">Serving Agents</a> · <a href="https://docs.kaelio.com/ktx/docs/guides/llm-configuration" target="_blank" rel="noopener noreffer">LLM configuration</a></p>
<h2 id="the-workflow-from-source-to-context-layer">The Workflow: From Source to Context Layer</h2>
<p>From reading configured source connectors (1) from databases, BI tools, modeling code, docs and notes, to context building and extracting the context (2) of each, to reconciliation (3) of creating new semantics and metrics based on it, to validation (4) and checking references before agents rely on them.</p>
<p>Resulting in a wiki and semantic layer that is referenced and self-improving, and this is how the semantic layer part looks within ktx:</p>













  
<figure><a target="_blank" href="/blog/agentic-context-layer/ktx-semantic-layer-process.webp" title="">

</a><figcaption class="image-caption">From <a href="https://docs.kaelio.com/ktx/docs/concepts/semantic-layer-internals" target="_blank" rel="noopener noreffer">Semantic querying, ktx docs</a></figcaption>
</figure>
<h2 id="why-a-governed-context-layer-matters">Why a Governed Context Layer Matters</h2>
<p>Wrapping up, I know this has been a lot of new information, but I&rsquo;m very excited about the direction of <a href="https://github.com/Kaelio/ktx" target="_blank" rel="noopener noreffer">ktx</a> and integrating logical layers such as metrics and semantic layers with more advanced business context, and with everything being fully open source for you to try out.</p>
<p>But the challenge of natural language for semantic layers being too imprecise is real. A context layer with hard and soft defined configurations will help the conversational interface via the agents tremendously. More context doesn&rsquo;t always help, but if it&rsquo;s well maintained and fetched from the actual source or pulling in actual domain knowledge from Notion pages, it&rsquo;s priceless.</p>
<p>Also, the feature of easily automating commands via CLI and adding or fixing wrong context in a central repository quickly is super helpful. This keeps the human in the loop and provides a consistent way to verify a governed layer that holds the company&rsquo;s context while using the power of agentic engineering.</p>
<p>If you like this, try ktx with the <a href="https://docs.kaelio.com/ktx/docs/getting-started/quickstart" target="_blank" rel="noopener noreffer">getting started guide</a>, and <a href="https://github.com/Kaelio/ktx" target="_blank" rel="noopener noreffer">star it on GitHub</a>. It&rsquo;s all open source.</p>
<hr>
<pre class=""><em>Full article published at <a href="https://www.kaelio.com/blog/building-a-context-layer-for-the-agentic-era" target="_blank" rel="noopener noreferrer">Kaelio.com</a> - written as part of <a href="/services">my services</a></em></pre> 
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>the wiki component helps, too, but the semantic layer is key for the speedup&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
</item>
<item>
    <title>The Dagster Almanack: From Complexity to Composability</title>
    <link>https://www.ssp.sh/blog/dagster-almanack-open-data-platform/</link>
    <pubDate>Tue, 26 May 2026 08:00:08 &#43;0200</pubDate>
    <author>Simon Späti</author>
    <guid>https://www.ssp.sh/blog/dagster-almanack-open-data-platform/</guid><enclosure url="https://www.ssp.sh/blog/dagster-almanack-open-data-platform/featured-image.png" type="image/png" length="0" /><description><![CDATA[<p>I have read the &ldquo;Poor Charlie&rsquo;s Almanack&rdquo; by Charlie Munger and thought about what it would take to write one for Dagster. A complete guide with all the insights, tips, and some predictions for the data platform engineer, just like an Almanack provides, with practical information for daily life.</p>
<p>My goal is to offer a collection of wisdom, insights, and principles gathered over the years. Giving you an outside view from someone who has used Dagster since back in 2019, used it at enterprise scale but also for my hobby projects (e.g. <a href="https://github.com/ssp-data/practical-data-engineering" target="_blank" rel="noopener noreffer">real-estate project</a>). The piece should give you a holistic view of Dagster&rsquo;s place in the data ecosystem, how to deal with the complexity of data architecture and enterprises, and scaling your data jobs.</p>
<p>This article shows you how orchestrators such as Dagster are built for an open data platform that integrates the full data ecosystem, with the shift to data assets instead of DAGs, reducing complexity and applying data engineering best practices.</p>
<blockquote>
<p>[!note] Definition of Almanack (also spelled &ldquo;almanac&rdquo;)<br>
The term refers to a publication containing a variety of information on a dedicated topic. The modern usage of Almanack, particularly in the context of books like those by Charlie or Naval Ravikant, is often metaphorical. It suggests a collection of wisdom, insights, or principles gathered over time.</p>
</blockquote>
<h2 id="what-is-dagster">What is Dagster</h2>
<p>In late 2018, on a co-working and co-living sabbatical in Bali, I was searching for something to bring the data warehouse out of the drag-and-drop world of SSIS and Oracle reporting and into a code-first, developer-friendly workflow. I looked at <a href="https://github.com/OptimalBI/optimal-data-engine-mssql" target="_blank" rel="noopener noreffer">ODE</a>, BiGenius, TimeXtender, and WhereScape, but found that none of them quite fit my open source and programmatic preferences, so I tried to build something myself but didn&rsquo;t succeed. A year later, back at my 9-to-5 in Copenhagen, I heard Nick Schrock on the <a href="https://www.dataengineeringpodcast.com/dagster-data-applications-episode-104/" target="_blank" rel="noopener noreffer">Data Engineering Podcast</a> describing the motivation and story behind a Python framework called Dagster that did exactly that. I was hooked, and have used Dagster ever since.</p>
<h3 id="early-focus-on-developer-friendliness">Early Focus on Developer Friendliness</h3>
<p>To understand the context of 2019, you must understand that back then, most ETL jobs were triggered with cron or bash scripts, and if there was an error, the only option was to re-run in the next nightly window where production wasn&rsquo;t touched. Dagster, as explained by Nick in the podcast, focused on developer-friendliness, in particular for ETL developers back then, and that focus hasn&rsquo;t changed today for data engineers.</p>
<p>So what is Dagster? The original idea, started in 2018 during a sabbatical after Nick worked at Facebook, came with this definition:</p>
<blockquote>
<p>One of the goals of Dagster has been to provide a tool that <strong>removes the barrier between pipeline development</strong> and pipeline operation, but during this journey, he came to <strong>link the world of data processing with business processes</strong>.</p>
</blockquote>
<p>Today the definition hasn&rsquo;t changed much and reads like this from the <a href="https://docs.dagster.io/" target="_blank" rel="noopener noreffer">Dagster Docs</a>:</p>
<blockquote>
<p>Dagster is a data orchestrator <strong>built for data engineers</strong>, with <strong>integrated</strong> lineage, observability, a declarative programming model, and best-in-class <strong>testability</strong>.</p>
</blockquote>
<p>The initial definition to &ldquo;link data processing with business&rdquo; was the key reason that brought me to it, along with the quality of how the components were implemented. Even more compelling was Nick&rsquo;s visionary outline for 3-5 years ahead: to make the work of data engineers similar to software engineers, and make their daily life easier.</p>
<h2 id="biggest-shift-early-on">Biggest Shift Early On</h2>
<p>This vision led to many new concepts Dagster originally created, which we take for granted in today&rsquo;s data work, and shifted the work into a more reliable and useful toolset for data engineers.</p>
<h3 id="data-aware-orchestration-shift">Data-aware Orchestration Shift</h3>
<p>One of the biggest shifts compared to previous tools and orchestrators was that orchestration was fully data-aware from the very beginning. It tried to understand the heterogeneous complexity that exists at every small to large enterprise company, and thrive in it, supporting the full data engineering lifecycle with its platform and data pipeline capabilities built in.</p>
<p>This gave me a toolkit for building reliable data pipelines out of the box early on, with battle-tested features through its users (open-source) and a quality and thoughtfulness I hadn&rsquo;t seen before. This was personified by Nick and could be vividly felt in the early interviews, but also in the code that the team produced openly on the repo.</p>
<p>For example, backfilling, restartability, or Spark integrations were open on GitHub, to use, and to adapt to your needs. It was a bit like dbt, but instead of modeling your SQL queries, you&rsquo;d model your data pipelines and integrate complex data architecture.</p>
<p>This also meant that moving datasets or integrating dependencies is strongly supported, not an afterthought. In Dagster you can use <a href="https://docs.dagster.io/dagster-basics-tutorial/resources" target="_blank" rel="noopener noreffer">resources</a> to work with Polars, Pandas, Arrow, DuckDB, or anything else to pass datasets, and reference data assets declaratively, or even non-existing ones as Dagster knows to create the assets. Compared to Airflow, where you could load only small data with XCom for the longest time<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, this makes code simpler to understand and maintain.</p>
<p>Resources also decouple storage from compute. You could use Apache Spark locally with a single JAR file and in test use a full-blown cluster, or use MinIO as an S3 interface with simple bucket configuration and in production an S3 server from Amazon. Both are <strong>interchangeable without changing pipeline logic, by pure configuration</strong> — that&rsquo;s the beauty of declarative data systems, and Dagster embraces this to this day.</p>
<h3 id="shift-to-data-asset-based-orchestration">Shift to Data-Asset Based Orchestration</h3>
<p>Dagster was super early in shifting from DAGs and operational task-based orchestration to asset-based ones such as BI dashboards, tables, reports, ML models — artifacts the user actually cared about. Everything shifted from an imperative approach to a declarative one like Kubernetes or React, where you define what the dataset must or should have, and Dagster takes care of the implementation logic, <strong>mapping it to the configured filesystem, compute engine or cluster</strong>.</p>
<p>Now you could quickly describe each pipeline with declarative notations like <code>update: daily</code> or change to <code>update: monthly</code>, or if more advanced, you could define a small <a href="https://docs.dagster.io/guides/automate/sensors" target="_blank" rel="noopener noreffer">sensor</a> logic that checks S3 for updates. Instead of implementing all logic in a data pipeline, we <strong>apply the logic directly to the asset</strong> and closer to the data, which makes it more transparent and integrated into the full <a href="https://dagster.io/glossary/data-lineage" target="_blank" rel="noopener noreffer">data lineage</a>. When updates happen, we have the full graph, but also a leaner and easier-to-maintain setup.</p>













  
<figure><a target="_blank" href="/blog/dagster-almanack-open-data-platform/dag-to-assets.webp" title="">

</a><figcaption class="image-caption">Going from typical DAG and task-based-oriented (first line) to asset-based DAG (exploded view) | More at <a href="https://www.youtube.com/watch?v=YYeTQJYvqjU&amp;t=408s" target="_blank" rel="noopener noreffer">Declarative Orchestration</a></figcaption>
</figure>
<p>Or zoomed out - you see the focus on the assets, the tables themselves:<br>













  
<figure><a target="_blank" href="/blog/dagster-almanack-open-data-platform/assets-view.webp" title="">

</a><figcaption class="image-caption">Global asset lineage, zoomed out. You see how the task-based view goes from function to function, from download to serve (each of which potentially hides multiple tables), where assets go from dataset to dataset, giving you much more information. | More at <a href="https://youtu.be/L5kTxCM-tOk?si=4Fh_zc0oTRHckrs8&amp;t=133" target="_blank" rel="noopener noreffer">Dagster Data Orchestration walkthrough</a></figcaption>
</figure></p>
<p>This led later to &ldquo;<strong><a href="https://dagster.io/blog/software-defined-assets" target="_blank" rel="noopener noreffer">Software-Defined Asset</a></strong>&rdquo; and its mental model where you can define an asset pre-runtime and declare connections to upstream or downstream data assets (e.g. real-time housing prices that we fetch from a webpage that does not exist beforehand). Now we can already build and implement our graph and data lineage without having to physically create the dataset first. Software-defined assets use code to define the data assets and are version-controlled through git and inspectable via tooling. This transparency allows anyone in your organization to <strong>understand the canonical set of data assets</strong> and reproduce them at any time, and also lays the groundwork for asset-based orchestration.</p>
<blockquote>
<p>As Rich Hickey <a href="https://www.youtube.com/watch?v=SxdOUGdseq4" target="_blank" rel="noopener noreffer">said</a>, the aesthetics of a programming language do not matter, only the outcome. Assets play into that fact. In data engineering terms, it&rsquo;s not code but data pipelines and DAGs, but what everyone cares about are their outcomes: the data assets.</p>
</blockquote>
<p>That led to the shift from working with tasks and DAGs to data assets. That&rsquo;s the developer-friendliness built in from day one: develop locally and deploy to test and production, with infrastructure and technical implementation decoupled from business logic.</p>
<p>It made Dagster a bit more complex to start with — you need to know more upfront — but since every enterprise hits these data engineering challenges eventually, it&rsquo;s better to embrace the fact and build for it. The result was an improved developer velocity, but what I noticed, too, was the joy of building reliable data pipelines, equipped with tools that helped me deal with errors, infrastructure, multi-tenancy, data science, big data, and everything thrown at me back then.</p>
<h3 id="not-only-single-purpose">Not only single Purpose</h3>
<p>When I first introduced Dagster at my previous company, all of a sudden other teams started to take notice and also wanted Dagster for other work such as provisioning infrastructure with one-click deployment. Especially the cloud platform team needed a tool to automate its scripts to deploy on Kubernetes, OpenShift, and everywhere else, but other teams also had needs to automate. With Python as the programming language for Dagster, reading from an external FTP server, transforming the data, and uploading it somewhere via API were not multi-month projects across different teams. Dagster&rsquo;s flexibility and <a href="https://docs.dagster.io/integrations/libraries" target="_blank" rel="noopener noreffer">integration</a> into other tools and systems were a key strength for most teams.</p>
<p>Observability and monitoring were another addition. Every run was logged in the UI and everyone could see the rich metadata of each pipeline run. And because it was open source and had a rapidly growing community, support and ideas didn&rsquo;t run out.</p>
<h2 id="dealing-with-the-complexity-of-enterprise-systems">Dealing with the Complexity of Enterprise Systems</h2>
<p>If you have worked at any company larger than 10 people, you have noticed pretty fast that you are dealing with multiple source systems, different CRMs, different ERPs, multiple cloud platforms. Most enterprises have all major cloud platforms running in production, whether it is Amazon services, Google GCP, Azure, or any other major platform. You as the data engineer are the one making sure to integrate them, and basically <strong>deal with the complexity that comes with it</strong>.</p>
<h3 id="how-to-reduce-complexity">How to Reduce Complexity?</h3>
<p>First, acknowledge it: heterogeneous data complexity is a fact of the enterprise data lifecycle. Second, lean on tooling with technical integrations and written code that implements each vendor&rsquo;s API, so we don&rsquo;t build everything from scratch repeatedly. Third, work around the actual data assets the users want, not DAGs. With assets we declare outcomes, tests, and dependencies, and the system handles the rest. That cuts dependency hell, an unproductive <a href="https://aws.amazon.com/what-is/sdlc/" target="_blank" rel="noopener noreffer">Software Development Lifecycle</a>, and the fear of change.</p>
<h3 id="composable-is-making-systems-simpler">Composable is Making Systems Simpler</h3>
<p>A more holistic framing comes from <a href="https://www.youtube.com/watch?v=SxdOUGdseq4" target="_blank" rel="noopener noreffer">Simple Made Easy</a> by Rich Hickey, creator of the Clojure functional programming language, where he debates what makes systems complex: state and objects, lots of vars, syntax, inconsistency. His conclusion is that <strong>composable</strong> is what makes systems simpler (like in music for a composer, which is what he created Clojure for): the ability to <strong>assemble, reassemble, and swap individual components</strong> into a flexible whole.</p>
<p>






</p>
<p>Dagster has exactly that ability, too. It integrates the core principles of <a href="https://maximebeauchemin.medium.com/functional-data-engineering-a-modern-paradigm-for-batch-data-processing-2327ec32c42a" target="_blank" rel="noopener noreffer">Functional Data Engineering</a> — pure and idempotent tasks, immutable partitions, reproducibility, versioning — directly into the framework.</p>
<p>State, Hickey points out, is never simple. Unfortunately for us, data engineering is <em>all</em> state: every datum is tied to a timestamp of when it was created, processed, or backfilled. Fortunately, helping us manage that state is what Dagster does: data assets, <a href="https://docs.dagster.io/guides/build/assets/virtual-assets" target="_blank" rel="noopener noreffer">virtual assets</a>, partitions, incremental materialization, and <a href="https://dagster.io/blog/dagster-1-13-octopuss-garden" target="_blank" rel="noopener noreffer">partitioned asset checks</a> that evaluate a specific partition of an upstream asset instead of the whole dataset.</p>
<p>Rich concludes &ldquo;<strong>Simplicity is a choice</strong>&rdquo;, echoing Leonardo Da Vinci:</p>
<blockquote>
<p>Simplicity is the ultimate sophistication</p>
</blockquote>
<h2 id="open-data-platform">(Open) Data Platform</h2>
<p>But how did the shift and the data engineering principles evolve, and how can we apply these as a unified solution?</p>
<p>With Dagster&rsquo;s data-aware orchestration, shift to assets, separation of concerns, and multi-use, we are automating harder data and infrastructure deployment problems, and Dagster solves the problem of managing complex data environments more holistically. To me, it feels as though Dagster gave me this peace of mind and the toolset to simplify data engineering in a complex environment early on, and is the right tool for the ultimate sophistication for data work.</p>
<p>Dagster&rsquo;s approach is composing a data orchestrator that integrates into any type of data work: from data integration with dlt, to transformation with dbt or just Python logic, to updating BI dashboards, to deploying on Kubernetes, all into a unified system. A fully <strong>open data platform</strong>, making <strong>orchestrating data and its flow <em>simpler</em></strong>.</p>
<p>All of these features, combined with DevOps deployment strategies, make Dagster one of the data platform tools that has:</p>
<ul>
<li>An <a href="https://docs.dagster.io/guides/operate/webserver#dagster-ui-reference" target="_blank" rel="noopener noreffer">integrated UI</a> and control plane for seeing what&rsquo;s going on, unifying all your tools into a single webpage.</li>
<li>Lets you <a href="https://docs.dagster.io/guides/operate/webserver#assets" target="_blank" rel="noopener noreffer">see your data assets</a> in a list with extensive metadata: it&rsquo;s your data catalog showing all tables, BI dashboards, reports, and other data assets.</li>
<li>Keeps and creates all the metadata when we run data pipelines across all data systems: With end-to-end access, we can also have metadata and data lineage end to end, which helps us understand where the data comes from, and in case of error, where the bad data is.</li>
<li>Has integrated scheduling, sensors, backfills, and <a href="https://docs.dagster.io/getting-started/concepts" target="_blank" rel="noopener noreffer">concepts</a> to work with data, built in.</li>
<li>Supports multi-team isolation through code locations, so different teams can own different parts of the platform without stepping on each other.</li>
</ul>
<p>As Dagster is open source, it gets promoted from a usual data orchestrator to an <strong>open data platform</strong>, with the great advantage of transparency: easily patching an error or integration if you need to integrate an obscure system that only your company has, working on the cutting edge with the community, or getting features from them.</p>
<blockquote>
<p>[!note] The Venn diagram of Dagster<br>
Obviously you can&rsquo;t optimize a data platform in all directions. If you look at Dagster as a Venn diagram, it has these three circles: the right <strong>abstraction, flexibility, and full automation</strong> through programmability.</p>
</blockquote>
<h3 id="control-plane-center-with-all-metadata">Control Plane: Center with All Metadata</h3>
<p>It integrates multiple different teams such as data engineers, platform and infra teams, with data science and business people who want to run their jobs. Feature-wise, it provides <strong>data catalog</strong> and contract capabilities, lets you see data assets you&rsquo;re responsible for and when they last got updated, and shows their downstream and upstream dependencies, all in one real-time observability and monitoring UI.</p>
<p>This is all done through its <strong><a href="https://youtu.be/rB2nNEEIRBE?si=nhnhaSGqIt7fd6pf&amp;t=743" target="_blank" rel="noopener noreffer">control plane</a></strong>, which centers all metadata and unifies different data tools along the lifecycle into one platform, something usually only closed-source data platforms achieve. The control plane serves everything in a <strong>single view</strong>, showing how all processes are working. It&rsquo;s the operational dashboard for your company.</p>
<p>With data orchestration as the heart of data work, with metadata for any process and access to all source systems we&rsquo;re pulling from or intermediate systems, it&rsquo;s in the perfect place to serve as the central metadata store. Think of INFORMATION_SCHEMA, but for overall data work, not only one single database. Only the orchestrator can understand the system and its status this deeply.</p>
<h3 id="why-open-data-platform-a-system-that-unifies-open-source">Why Open Data Platform: a System that Unifies Open Source</h3>
<p>The <em>open</em> in open data platform is interesting, as it&rsquo;s really hard to build a unifying layer across different stateful data systems, and it&rsquo;s worth highlighting that Dagster achieved just that.</p>













  

























<figure>
<a target="_blank" href="/blog/dagster-almanack-open-data-platform/concept-dagster-open-platform.png" title="Open Data Platform with Dagster as the integrative data orchestrator into different layers of the data stack — built on open standards, with an open data architecture such as object storage (S3 specs), file formats (Parquet, ORC, Avro), open table formats (Iceberg, Delta, Hudi), and data catalogs. | Legend: Dark blue shaded : Part of Dagster (control plane, orchestration, etc.), light blue: dagster managed metadata, white: external state and systems">

</a><figcaption class="image-caption">Open Data Platform with Dagster as the integrative data orchestrator into different layers of the data stack — built on open standards, with an open data architecture such as object storage (S3 specs), file formats (Parquet, ORC, Avro), open table formats (Iceberg, Delta, Hudi), and data catalogs. | Legend: Dark blue shaded : Part of Dagster (control plane, orchestration, etc.), light blue: dagster managed metadata, white: external state and systems</figcaption>
</figure>
<p>If we look at open data stack architecture: unlike cloud data platforms that have the same goal, such as Fabric, Snowflake, Databricks, etc., Dagster builds on <strong>open standards</strong> and is itself an open standard. It&rsquo;s like a protocol in which we declaratively define our data assets (e.g. Software-Defined Assets, environments, resources) that then get automatically executed with composable computes we define in resources, all interchangeable. Even closed-source engines such as a Databricks Spark cluster work really well.</p>
<p>The hardest part is integrated data governance, lineage, access rights, and compute. An orchestration platform like Dagster doesn&rsquo;t give you everything, but you get most of what you need, in an open and composable way.</p>
<h3 id="composable-data-stacks-possible">Composable Data Stacks Possible</h3>
<p>What this architecture allows is what Wes McKinney calls &ldquo;Composable Data Stacks&rdquo; in <a href="https://open.spotify.com/episode/4yEBsHs75QyxnQqK11ghyC?si=2c7861fde2354a52" target="_blank" rel="noopener noreffer">Monday Morning Data Chat</a>, essentially <a href="https://dagster.io/blog/rebundling-the-data-platform" target="_blank" rel="noopener noreffer">rebundling the data platform</a>.</p>
<p>Composable data stacks depend on compute engines. With this architecture, plus Dagster resources or <a href="https://docs.dagster.io/integrations/external-pipelines" target="_blank" rel="noopener noreffer">external code (Dagster Pipes)</a>, we can easily pick and choose what is best suited for the task at hand, not only for different jobs but also depending on different environments. Although there will never be one singular tool for everything, it&rsquo;s necessary that we have a layer of integration, and there&rsquo;s no better place than the orchestration layer that separates execution and technical logic from business and already deals with multiple data environments.</p>
<p>Pete Hunt, the CEO of Dagster, <a href="https://www.linkedin.com/feed/update/urn:li:activity:7447649923132481536?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7447649923132481536%2C7447978059514925056%29&amp;dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287447978059514925056%2Curn%3Ali%3Aactivity%3A7447649923132481536%29" target="_blank" rel="noopener noreffer">said recently</a>:</p>
<blockquote>
<p>Our goal is to make AI as lightweight, accessible and cheap as we can to drive enterprise value, while enabling our customers with the infrastructure they need underneath - the orchestration and <strong>data platform layer where reliability, support, and recovery</strong> actually matter.</p>
</blockquote>
<p>Pete is reaffirming that the data platform layer is the priority, so AI can build on top of a great foundation. In the eyes of a data engineer, it&rsquo;s a dream come true to have an open platform that is declarative to simplify the overall architecture, but also uses AI for the glue code, based on <strong>best practices enforced through Dagster as the open data platform</strong>.</p>
<h2 id="the-right-abstraction-layer-for-an-open-data-platform">The Right Abstraction Layer for an Open Data Platform</h2>
<p>Charlie Munger&rsquo;s Almanack distilled decades of investing wisdom into timeless principles and mental models that compound over time. Here I tried the same for Dagster.</p>
<p>The principles touched on won&rsquo;t be obsolete next year. Data-aware orchestration, declarative assets over imperative DAGs, separation of business logic from infrastructure, and <strong>composable stacks with a single control plane</strong> are all mental models for building data platforms that hold up whether you&rsquo;re running DuckDB on a laptop or Spark across three cloud providers.</p>
<p>Eight years after discovering Dagster on a podcast during my time in Copenhagen, I&rsquo;m still reaching for it whenever a system gets complex enough to need real orchestration. With extensive built-in <a href="https://docs.dagster.io/guides/operate/configuration/advanced-config-types#union-types" target="_blank" rel="noopener noreffer">data quality checks</a>, <a href="https://docs.dagster.io/examples/best-practices" target="_blank" rel="noopener noreffer">best practices</a> like <a href="https://docs.dagster.io/guides/test/unit-testing-assets-and-ops" target="_blank" rel="noopener noreffer">unit-testing</a>, <a href="https://docs.dagster.io/guides/operate/configuration/using-environment-variables-and-secrets#per-environment-configuration" target="_blank" rel="noopener noreffer">local development to prod</a>, separation of business and technical logic, <a href="https://docs.dagster.io/examples/best-practices/shared-module" target="_blank" rel="noopener noreffer">code locations</a>, <a href="https://dagster.io/blog/dsls-to-the-rescue" target="_blank" rel="noopener noreffer">Domain Specific Languages (DSLs)</a> for non-technical people, <a href="https://docs.dagster.io/integrations/external-pipelines" target="_blank" rel="noopener noreffer">pipes</a> and <a href="https://docs.dagster.io/getting-started/concepts#component" target="_blank" rel="noopener noreffer">components</a> to run something in Rust or Go, and <a href="https://docs.dagster.io/getting-started/concepts" target="_blank" rel="noopener noreffer">many more</a>, the Dagster data platform gives you huge leverage building from strong foundations, with the flexibility to change along the way.</p>
<p>It&rsquo;s the abstraction layer for data engineering to solve hard business problems, an open data platform with opinionated design decisions that compound the longer you build on them.</p>
<p>In the next piece, I&rsquo;ll get into what it actually takes to operate this — architecture, deployment, and governance — as a follow-on to these principles.</p>
<h2 id="next-steps">Next Steps</h2>
<p>Find <a href="https://github.com/dagster-io/skills" target="_blank" rel="noopener noreffer">Dagster&rsquo;s official skills</a> for the latest and most updated way of working with Dagster, to feed to your AI agent. Or read the <a href="https://dagster.io/blog/evaluating-agent-skills" target="_blank" rel="noopener noreffer">blog post</a> with more information. If using Airflow, <a href="https://docs.dagster.io/migration/airflow-to-dagster" target="_blank" rel="noopener noreffer">migrate from Airflow</a>, or use <a href="https://docs.dagster.io/integrations/libraries/airlift" target="_blank" rel="noopener noreffer">Airlift</a> for an integration for legacy and critical DAGs still in Airflow.</p>
<p>Find <a href="https://github.com/dagster-io/awesome-dagster" target="_blank" rel="noopener noreffer">awesome-dagster</a>, and check out further readings of mine at <a href="https://www.ssp.sh/blog/data-integration-as-code-airbyte-dbt-python-dagster/" target="_blank" rel="noopener noreffer">Data Integration as Code: Configuring Airbyte and dbt with Python (Dagster)</a> or <a href="https://www.ssp.sh/blog/data-orchestration-trends/" target="_blank" rel="noopener noreffer">Data Orchestration Trends: The Shift From Data Pipelines to Data Products</a>.</p>
<p>Want to use all of this stress-free without the deployment burden? Use <a href="https://dagster.io/lp/dagster-plus-trial" target="_blank" rel="noopener noreffer">dagster+</a>. Great tradeoff between cloud and OSS, still having the OSS Dagster foundation, but profiting from extra features (GitHub integration, cloning, etc.) and not needing to set up a DevOps pipeline or fiddle with Kubernetes.</p>
<hr>
<pre class=""><em>Full article published at <a href="https://dagster.io/blog/the-dagster-almanack-from-complexity-to-composability" target="_blank" rel="noopener noreferrer">Dagster.io</a> - written as part of <a href="/services">my services</a></em></pre>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Since August 2024 and version 3.0, Airflow finally supports a declarative approach, inspired by Dagster too. Airflow now has data-aware orchestration with <a href="https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/assets.html" target="_blank" rel="noopener noreffer">Asset Definitions</a>:&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
</item>
<item>
    <title>Plan Mode All the Time, Substrait over SQL, and the End of the DE Role ft. Chris Riccomini</title>
    <link>https://www.ssp.sh/blog/how-to-use-ai-with-de-chris-riccomini/</link>
    <pubDate>Tue, 26 May 2026 08:00:08 &#43;0200</pubDate>
    <author>Simon Späti</author>
    <guid>https://www.ssp.sh/blog/how-to-use-ai-with-de-chris-riccomini/</guid><enclosure url="https://www.ssp.sh/blog/how-to-use-ai-with-de-chris-riccomini/featured-image.png" type="image/png" length="0" /><description><![CDATA[<p>This series interviews (see <a href="/blog/specs-over-vibes-interview-mark-freeman/" rel="">#1 with Mark Freeman</a>) real practitioners to extract the patterns behind how they actually use AI in their data work today. This is the second interview in &lsquo;How to use AI with DE&rsquo;, and this time we have none other than <a href="https://www.linkedin.com/in/riccomini/" target="_blank" rel="noopener noreffer">Chris Riccomini</a>.</p>
<p>Chris has seen the data stack evolve over the years. He thinks AI will soon handle the majority of data engineering work, provided with the right tooling and access to CLIs and APIs. He also thinks LLMs might not speak SQL, but a format that represents data transformations. With so much shifting and changing currently in the AI space, new models, new workflows weekly, Chris&rsquo;s perspective helps you navigate without overreacting, based on a long experience in the domain.</p>
<p>The article is structured in four parts: <strong>(1)</strong> correctness when working with financial data, <strong>(2)</strong> the Ralph Loop and why AI might be better off speaking something other than SQL, <strong>(3)</strong> vulnerabilities and the case for &ldquo;Okta for Agents,&rdquo; and <strong>(4)</strong> the future of AI — including why &ldquo;data engineer&rdquo; as a distinct role might not survive.</p>
<h2 id="introducing-the-guest-2-chris-riccomini">Introducing the Guest: #2 Chris Riccomini</h2>
<p>Chris Riccomini is a Software Engineer, Author, <a href="https://materializedview.capital/" target="_blank" rel="noopener noreffer">Investor</a>, and Advisor. Previously at WePay, LinkedIn, PayPal, and author of <a href="https://www.amazon.com/Missing-README-Guide-Software-Engineer/dp/1718501838" target="_blank" rel="noopener noreffer">The Missing README: A Guide for the New Software Engineer</a> and co-author of 2nd version of the iconic <a href="https://www.amazon.com/dp/1098119061" target="_blank" rel="noopener noreffer">Designing Data-Intensive Applications</a> book.</p>
<p>Chris has been working in open source throughout his career. He is the author of <a href="https://github.com/apache/samza" target="_blank" rel="noopener noreffer">Apache Samza</a>, a distributed stream processing framework. His current project is SlateDB, an embedded key-value store built on object storage. He is also on the <a href="https://projects.apache.org/committee.html?airflow" target="_blank" rel="noopener noreffer">Apache Airflow&rsquo;s PMC</a>.</p>
<h2 id="correctness-of-data-in-the-financial-sector-how-does-this-work-with-ai">Correctness of Data in the Financial Sector: How Does This Work with AI?</h2>
<p>Chris had worked at financial companies where <strong>data correctness</strong> is essential. My first question was &ldquo;How do you see using AI in data when financial services, or most other places, must be correct? How do you mitigate the small errors AI still makes in such a situation?&rdquo; His response:</p>
<blockquote>
<p>It really depends on where in the stack AI is being deployed.</p>
</blockquote>
<h3 id="use-cases-with-different-risk-profiles">Use Cases with Different Risk Profiles</h3>
<p><strong>Risk, fraud and compliance</strong>. The bar is model explainability, you need to know <em>why</em> the model made the decision it did:</p>
<blockquote>
<p>If AI is involved in decisioning around risk and fraud, compliance and “model explainability” comes into play (why the model made the decision it did). This is one of the reasons we really liked random forest models at WePay: you could explain the actual rules that the model had derived and used in order to make a decision.</p>
</blockquote>
<p>The <strong>data engineering context</strong>, compared to a traditional data modeling situation, is interesting:</p>
<blockquote>
<p>If AI is being used in a data engineering context, it seems to me more like a <strong>traditional data modeling situation</strong>. You should be able to define invariants that must always be true for your data. For example, the ledger should always sum up. This is how we managed our data pipelines. If AI is defining data integration pipelines and moving data, the invariants should still hold. Traditional data verification tools will continue to play a role there.</p>
</blockquote>
<p>For <strong>data analytics</strong>, this is where most of the fear lives:</p>
<blockquote>
<p>There is a fear that AI will hallucinate and cause a bad decision to be made. <strong>I think this is a reasonable fear, but it’s also a problem we had before AI.</strong> Data in any organization is messy. Semantics aren’t always clear, contracts get broken, and so on.* *Every company I’ve worked for has had this problem. It’s <strong>not uncommon to find an incorrect query</strong> that’s been rolled up into a weekly ops review with the CEO, for example. This was true before AI.</p>
</blockquote>
<p>So the question, is whether AI makes this worse or better. Chris own view has shifted recently:</p>
<blockquote>
<p>If you’d asked me two years ago, I would have said it was definitely going to get worse. Now, I think it might actually get better, especially if we <strong>pair AI with a human</strong>. The latest LLMs have gotten really good at spotting bugs, inconsistencies, and so on. My personal experience is that I’m both <strong>more productive and more accurate with an AI</strong>.</p>
</blockquote>
<p>I am having a similar experience: for working data engineering projects, if I use it for a not-too-distant future, meaning if the scope is clear and in a framework or rigid structure, it can implement a great solution since last December 2025, when the models got better. With it, it can go a long way, but still, it can&rsquo;t work autonomously, or do a full project from scratch. It still needs a lot of hand-holding, as it does not understand the business.</p>
<p>So, balancing quantity with quality and keeping up with reviews at the speed of generation is also a challenge, especially since the model usually generates many lines of code. But for my writing process, where my personal voice plays a bigger role, I find that AI can&rsquo;t help me too much yet in the actual writing process - but on the surrounding tasks (research, brainstorming, though also limited for new topics that are not based on existing ideas).</p>
<h3 id="llm-should-speak-substrait-not-sql">LLM Should Speak Substrait, not SQL</h3>
<p>Chris <a href="https://x.com/criccomini/status/1946674377153786327" target="_blank" rel="noopener noreffer">said recently</a> that: &ldquo;<em>Similar to my belief that LLM should speak substrait, not SQL</em>&rdquo;. I asked him to explain this quote and he said:</p>
<blockquote>
<p>This is more of an intuition than something I’ve demonstrated to be true. But if you look at the way we use SQL, it’s actually used in two different ways: <strong>by humans and by machines</strong>. I think both can benefit from <a href="https://substrait.io/" target="_blank" rel="noopener noreffer">Substrait</a> (or some equivalent).</p>
</blockquote>
<p>Chris continues to explain that &ldquo;<em><strong>Substrait is a format that represents data transformations</strong>. It has many operations that SQL has, but unlike SQL, which is purely logical, <strong>Substrait lets you define physical operations</strong> as well. In SQL, you say JOIN, but in Substrait you can say how to join: merge join or hash join? For those with a compilers background, Substrait can express both abstract and concrete syntax trees–intermediate representations (IRs).</em>&rdquo;</p>
<p>This is valuable for LLMs for two reasons:</p>
<blockquote>1. You should be able to <strong>express SQL with fewer tokens</strong> (provided the serialization format for the logical operations is more efficient than english). This should make LLMs slightly cheaper to use, but more importantly it should <strong>keep them from hallucinating quite as much</strong>. (Granted hallucinations are less of a problem than they used to be).<br><br>
2. More importantly, LLMs are pretty smart. They should be able to do query optimization really well. And Substrait <strong>gives them that ability–they can express physical operators</strong> (e.g. merge vs. hash), not just logical ones. This should allow them to do <strong>query optimization on the client side</strong>, and pass a physical query plan directly to the DB for execution (provided they have access to the requisite table statistics).</blockquote>
<p>Substrait, as an emerging standard that provides cross-language serialization for relational algebra, is very interesting and something I want to check out, especially the expressiveness compared to SQL.</p>
<blockquote>
<p>[!note] Downside of Substrait: LLMs are less familiar<br>
Of course, there is a ton of SQL on the internet, so it’s not clear that LLMs will be as amenable to working with lesser known formats like Substrait. I think it’s worth experimenting with, though.</p>
</blockquote>
<h2 id="making-ai-output-more-reliable">Making AI Output More Reliable</h2>
<p>What I learned is that the longer something is in the future, the more vague or incorrect or hallucinated the outcome can be. So the more context and code you can provide, the more accurate the result. Which is pretty much in line with Substrait.</p>
<p>But how do we work with the LLMs, what&rsquo;s the best approach, using <code>god mode</code> in OpenClaw or <code>--dangerously-skip-permissions</code> in Claude Code with no limits where it can go indefinitely with not much more context? I asked Chris if that&rsquo;s also what he observed, and if he uses <code>plan mode</code> and a declarative approach or pipelines, as it helps for context and collaborating with the AI on a shared output, usually Markdown.</p>
<blockquote>
<p>I was having coffee with a friend of mine, lamenting about this very problem a month or two ago. I was trying to get Codex to do something complex and it just kept falling on its face. My friend told me that you have to live in plan mode all the time. You can’t just ask it to plan the work, then flip to “Implement this plan.” You <strong>need to have the LLM iterate on the plan</strong> for many iterations. Probe its plan, ask it for details, ask it to expand sections, and so on. You need to get to the point where you feel like there’s no possible way the LLM can’t implement the plan incorrectly.</p>
</blockquote>
<blockquote>
<p>[!info] Spec-driven AI work</p>
<p>On the note of &ldquo;needing to have the LLM iterate on the plan for many iterations&rdquo;, Mark Freeman suggested in <a href="/blog/specs-over-vibes-interview-mark-freeman/" rel="">previous interview</a> the spec-driven development (SDD) approach with the open-source GitHub <a href="https://github.com/github/spec-kit" target="_blank" rel="noopener noreffer">Spec Kit</a>, check it out or read the previous interview for more context.</p>
</blockquote>
<h3 id="the-ralph-loop-and-managing-context">The Ralph Loop: And Managing Context</h3>
<p>After having a plan at hand, the next step is to keep the LLM&rsquo;s working memory lean:</p>
<blockquote>
<p>Once you have a good plan, you <strong>need to manage context</strong>. In some cases, you will need to take your plan and start with a fresh context in the LLM. In other cases, you’ll need to clear the context periodically throughout the work. I use a <a href="https://ghuntley.com/loop/" target="_blank" rel="noopener noreffer">Ralph Loop</a> for such cases<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
</blockquote>
<p>I had the exact same experience when working with smaller code bases: to refresh context, the insights you gain over the iterations are not as effective if you add them bit by bit, compared to if you refresh memory and start over with all the new key insights provided at the very beginning, steering the model to a more tailored direction earlier on.</p>
<p>But with the Ralph Loop, which refers to understanding AI beyond surface-level applications, you get new insights that you can then add to your initial prompt, that you wouldn&rsquo;t have gained otherwise, by exploring deeper programmable patterns.</p>
<p>The loop is an iterative, autonomous AI development technique where a bash loop (or plugin) repeatedly prompts an AI agent with the same goal, forcing it to persistently iterate until tasks pass external tests. It forces the AI to work, fail, and fix errors until success, rather than relying on the AI to decide it is finished.</p>
<p>On top of that, Chris says &ldquo;<em>You also need to impose a lot of quality gates. As with plan mode, you need to overdo it. &lsquo;Quality&rsquo; is a bit of a squishy term</em>&rsquo;&rdquo;, and he breaks it into three steps:</p>
<blockquote>1. Define what quality is for your use case.<br>
2. Measure the quality.<br>
3. Enforce thresholds (gates) that your LLM must adhere to.</blockquote>
<blockquote>
<p>[!example] Example by Chris with the three steps for assessing quality</p>
<p><em>For example, part of your definition of quality might be test coverage. So that’s step 1. Then you set up a coverage tool for your codebase–step 2. Then, you put the phrase, “You always run tests before commit and keep test coverage above 90%,” in your <a href="http://CLAUDE.md" target="_blank" rel="noopener noreffer">CLAUDE.md</a>. Finally, you install a git commit hook that enforces this rule.</em></p>
<p><em>This is a very rudimentary example, but you get the idea. There are a ton of different things you can measure and monitor for your work. I enumerate many in the post <a href="https://rng.md/posts/code-quality-for-vibe-coded-projects/" target="_blank" rel="noopener noreffer">Code Quality Gates for Vibe-Coded Projects</a>.</em></p>
</blockquote>
<p>This essentially means we as the Prompt Engineers need to make sure that the workflow is correct, that we understand what we need to do, and accordingly adapt the workflow to get better code quality.</p>
<h3 id="what-about-functional-data-engineering-and-executing-deterministically">What about Functional Data Engineering, and Executing Deterministically?</h3>
<p>In related terms, just as AI might hallucinate, it also might generate different outcomes with the same questions and same context. It&rsquo;s non-deterministic. But data engineering works especially well if it&rsquo;s done reproducibly, so we can backfill our data pipelines reliably and trust they will fill the same way.</p>
<p>This also ties into functional data engineering, running jobs with reproducibility and idempotent. I asked Chris what he thinks about this dilemma.</p>
<blockquote>
<p>I’m not as worried about this as I used to be. A lot of <strong>tooling</strong> has popped up or evolved to help address this. <strong>Durable execution frameworks</strong> try to address some of this by papering over the non-determinism to keep replays deterministic <strong>by skipping the previously-successful</strong> parts of the flow. Ditto for traditional workflow orchestration systems like Airflow, Prefect, and Dagster. (Disclaimer: I have some Prefect shares.)</p>
</blockquote>
<h3 id="moving-to-incremental-loads-for-better-determinism">Moving to Incremental-loads for Better Determinism?</h3>
<p>What I found interesting was Chris&rsquo;s next suggestion: moving to smaller data sizes, and therefore to loading incrementally for a more reproducible outcome.</p>
<blockquote>
<p>We can also move from full batch data processing to <strong>incremental batch</strong> data processing to help eschew some non-determinism.</p>
</blockquote>
<p>A concrete example, splitting load by day:</p>
<blockquote>
<p>Imagine, you have a bulk load job that always loads a full table from PostgreSQL into Snowflake, and that job does some LLM-based processing. Every time you re-run it, you’re going to get non-deterministic output. But if you convert it to an incremental job that runs daily and always loads the previous day’s data, then a re-run will only introduce non-determinism into the last day’s load. And presumably you’re re-running that day because something went wrong. In such a case, non-determinism is likely acceptable.</p>
</blockquote>
<p>This is great thinking and shows it&rsquo;s all about the use case and the risk appetite. If you have a lot less back reloads daily, compared to a full load, the accepted risk of one day might be acceptable, if you get great insights from the LLM, or something you&rsquo;d need to do manually and then the alternative would be you either don&rsquo;t do it at all, or very late when the insight is &ldquo;less&rdquo; valuable.</p>
<p>Side note, the engineering implementation of incremental loads might be much higher than a full load, as you need to add clear state management, checking what has run, and manage that state yourself, versus just running all. But this point almost certainly comes up in any case, whether you use AI or not, so we can factor out that fact in this scenario.</p>
<h2 id="how-to-prevent-vulnerabilities-and-work-securely-with-ai-agents">How to Prevent Vulnerabilities, and Work Securely with AI Agents?</h2>
<p>Another hot topic with agents is security concerns around vulnerabilities. I asked Chris how he sees that domain in combination with generative AI, and also if we need &ldquo;Okta for Agents&rdquo;, as Maxime Beauchemin <a href="https://www.linkedin.com/posts/maximebeauchemin_i-finally-got-to-around-to-test-driving-clawdbot-activity-7423272818848550912-FSCn?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABkA2pgBYM4xDO0z2ChYuxFhBfu4h7jp4Lo" target="_blank" rel="noopener noreffer">called</a> it.</p>
<p>His view splits cleanly in two:</p>
<blockquote>
<p>On the one hand, it’s a nightmare to manage these agents in the enterprise. On the other hand, they’re phenomenal at detecting compliance violations: leaked credentials, leaked PII, and so on.</p>
</blockquote>
<p>He&rsquo;d been thinking about an Okta-for-agents independently:</p>
<blockquote>
<p>It’s funny you mention Maxime’s “Okta for agents” comment. I didn’t see it, but I’ve been saying the exact same thing. It seems patently obvious to me. What’s unclear is whether Okta is Okta for agents, or whether another company (or companies) will take its place. Innovator’s dilemma and all. Okta’s certainly give it a good try–their homepage is covered in it now.</p>
</blockquote>
<h3 id="skills-marketplaces-and-mcps">Skills, Marketplaces and MCPs</h3>
<p>He continues and says that it&rsquo;s the wild west right now. You can load skills and even arbitrary skills from a marketplace and load any kind of text files without knowing if there&rsquo;s a vulnerability.</p>
<p>There are examples where hidden <a href="https://x.com/ZackKorman/status/2018386838101086446" target="_blank" rel="noopener noreffer">code injection</a> is done in a repo:<br>













  

























<figure>
<a target="_blank" href="/blog/how-to-use-ai-with-de-chris-riccomini/security-ingection.png" title="A hidden comment that is commented out below | source">

</a><figcaption class="image-caption">A hidden comment that is commented out below |  <a href="https://x.com/ZackKorman/status/2018386838101086446" target="_blank" rel="noopener noreffer">source</a></figcaption>
</figure></p>
<p>Chris continues with not having enough guardrails:</p>
<blockquote>
<p>But yes, we absolutely <strong>need lineage, auditability, RBAC, ABAC, and so on</strong>. It’s the wild west right now (as far as I know, anyway). This is one of the reasons I was so outspoken about MCP when it first came out. I was very <strong>disappointed in their (lack of) security model</strong>. It’s the most important part, and it was completely lacking. It was rather shocking to me given Anthropic’s focus on the enterprise. More recently, they’ve added better support, though, so credit where credit is due.</p>
</blockquote>
<h2 id="future-with-ai-agents">Future with AI Agents</h2>
<p>When asked about the future of AI, especially when we talk about data engineering, we discussed three interesting topics on what agents are doing well today, the role of data engineering itself and what programming language to use.</p>
<h3 id="what-agents-already-do-well-today">What Agents Already Do Well Today</h3>
<p>I asked if we get self-healing data pipelines, so we do not need to get up at night, meaning AI does not only detect errors, but also analyses, debugs, pushes a commit to the repo and re-runs the pipeline autonomously?</p>
<blockquote>
<p>I’ll be frank: I think AI will do the majority of the data engineering work in the future. I think we’re already at a point where it can; the tooling and practices just haven’t yet adapted.</p>
</blockquote>
<p>This is an interesting point regarding tooling (and practices) not being adapted yet. Jeff Dean, Chief Scientist at Google DeepMind, <a href="https://www.youtube.com/watch?v=g8BuAtM3fp4" target="_blank" rel="noopener noreffer">made the point</a> that Amdahl&rsquo;s Law still applies, and that we need to re-engineer our tools as they were designed for human speed. If AI agents can run 50x faster, but the tools don&rsquo;t, then we do not get an overall improvement.</p>
<p>On the other hand, what agents already do well today:</p>
<blockquote>
<p>Agents are already excellent at inspecting failed Github actions, failed workflows, running SQL queries, writing Python–all the things data engineers do. As they get plugged into monitoring systems and begin to auto-remediate, the grunt work of data engineering will get taken over by AI.</p>
</blockquote>
<p>And building new pipelines, given the right access:</p>
<blockquote>
<p>Agents are also fully capable of adding new data pipelines, provided they have access to infrastructure to do so. If you stand up a fresh Airflow and add connections for all your systems, I’d wager an Agent can set up as many pipelines as you need on it. And if you define the security and compliance policies it should follow, it’ll do so.</p>
</blockquote>
<p>Here, in my opinion, it is key that we use declarative and config-driven stacks, like Kubernetes and React are doing, and most modern tooling.</p>
<h3 id="data-engineering-role-going-away-or-unified">Data Engineering Role Going Away, or Unified?</h3>
<p>Continuing on the thread of the future of AI, Chris talks about how shifting left is a movement we had for a while, and where this leaves data engineers as a role:</p>
<blockquote>
<p>I’m not sure where that leaves data engineers. The “shift left” movement has been going on for a while. I can imagine a world in <strong>which “data engineer” as a distinct role goes away</strong>, or is folded back into a more generic data role that includes <strong>data engineering, machine learning, data analysis, and so on</strong>.</p>
</blockquote>
<p>He&rsquo;s been pushing this for <a href="https://materializedview.io/p/merge-analytics-and-data-engineers" target="_blank" rel="noopener noreffer">quite some time</a>:</p>
<blockquote>
<p>We over-specialized the data space. It might have been necessary, but it isn’t now. So perhaps we’ll see “data” be a single role that encompasses not just data engineering, but analysis and machine learning/AI as well. I think that would be healthy.</p>
</blockquote>
<h3 id="should-we-let-the-ai-agent-choose-the-language">Should We Let the AI Agent Choose the Language?</h3>
<p>We heard people saying (e.g. Wes McKinney) that they choose programming languages, in this case Go over Python, based on AI, not what the human prefers. He calls it <a href="https://wesmckinney.com/blog/agent-ergonomics/" target="_blank" rel="noopener noreffer">From Human Ergonomics to Agent Ergonomics</a>. That Wes, the creator of Pandas and author of Python for Data Analysis (stay tuned, he will be the next guest for this interview series), chose Go is interesting, and is because its advantages in fast compile-test cycles and painless software distribution are key. Don&rsquo;t worry, Python will not go away<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</p>
<p>Or Ladybird is <a href="https://ladybird.org/posts/adopting-rust/" target="_blank" rel="noopener noreffer">rewriting</a> part of the browser entirely from scratch in Rust with agents in two weeks. So Chris, do you think that choosing the programming language will depend on the ergonomics of the agents in the future (or now already)?</p>
<blockquote>
<p>In a word: yes. I have been pretty enthralled with the <strong>software factory concept</strong> lately. It’s how I do a lot of my development now. <strong>In that world, I just don’t care about the language</strong> my software is written in.</p>
</blockquote>
<p>What he optimises for instead:</p>
<blockquote>
<p>I care more about the characteristics of the output: its <strong>performance, stability, and cost to build</strong> (i.e. tokens). Languages that lend themselves to faster, cheaper, more stable LLM output are going to win.</p>
</blockquote>
<p>These are very interesting thoughts, and I did a project fully vibe coded in Go to experience the <strong>cost-as-tokens</strong> as well. The codebase kept being small (apart from the tests), and therefore I could go much further with the given tokens compared to other projects where I used the same Claude Plan Pro and ran out.</p>
<p>Go is a language I don&rsquo;t usually program in. And it is quite astonishing how far you get, but I also noticed a limitation as Lines of Code and size of the project grew, especially when adding new features that would break working features.</p>
<blockquote>
<p>[!note] Side note by Chris: No more proofs for a programming language required</p>
<p>I used to think that this would pull us in the direction of languages that have formal methods properties. Proofs, model checkers, and so on. I no longer think that’s the case, though. I think LLMs have gotten good enough at writing code that proofs are no longer required. We can use normal testing strategies. I wrote more about this in <a href="https://rng.md/posts/the-waymo-rule-for-ai-generated-code/" target="_blank" rel="noopener noreffer">The Waymo Rule for AI-Generated Code</a>.</p>
</blockquote>
<h3 id="does-ai-take-away-the-learnings">Does AI Take away the Learnings?</h3>
<p>Last question I asked Chris — the danger of not learning new things, and getting overwhelmed with constant stimulation, and even addicted? In a world where we only prompt, where we don&rsquo;t experience hitting a wall and then figuring it out, does that prevent us from learning new things? Are we just cruising on auto-pilot?</p>
<p>Chris mentions that it depends on how we use it and brings an example:</p>
<blockquote>
<p>One could argue a calculator makes us learn less math; indeed, I keep an eye on that with my middle school-aged kids. But it’s also a tool that lets us do far more complex math without worrying about carrying the one or shifting the decimal, so to speak.</p>
</blockquote>
<p>But you can also learn <em>with</em> AI he argues:</p>
<blockquote>
<p>I have had instances where I learn a ton from AI. A concrete example: <a href="https://github.com/slatedb/slatedb" target="_blank" rel="noopener noreffer">SlateDB</a>’s language bindings. I built them all from scratch (or rather, AI generated them all from scratch). When I started, I knew nothing about bindings. As I <strong>worked with AI to steer it and iterate on the code, I learned</strong> about cbindgen, UniFFI, foreign function interfaces (FFIs), and so on. It’s a phenomenal tool for picking up something from scratch. I can ask it questions, learn from it, and so on.</p>
</blockquote>
<p>Again, did he actually learn as much (from scratch, with AI) as he would have building it himself?</p>
<blockquote>
<p>Almost certainly not, I think <strong>I would have learned a lot more [without AI]. But I also wouldn’t have done the work</strong>. Writing four bindings (Node, Java, Python, and Go) from scratch is just too much work. I don’t have the time for it. Especially since I have never written a line of Go, and I know next to nothing about the Node ecosystem. So in the real world, I think I came out ahead.*</p>
</blockquote>
<h4 id="do-we-learn-fewer-things">Do We Learn Fewer Things?</h4>
<p>Let&rsquo;s finish with a question: Are we learning <em>fewer</em> or just <em>different</em> things? Something I&rsquo;ve wrestled with for a while. Chris&rsquo;s answer is:</p>
<blockquote>
<p>Perhaps the things we are no longer learning don’t really matter anymore. Going back to the calculator example, I couldn’t really tell you in detail how a calculator physically works. If you took it apart and showed me its circuitry, I’d be unable to tell you anything about it, really. Does that matter? I’m not so sure.</p>
</blockquote>
<p>I think we all are in this experience together, and nobody can really predict the future. I experienced both sides: when I rely too much on the assistant, I get more lazy and do the <em>deep thinking</em> less. While I course-corrected, and only used it for dedicated tasks, I noticed that abilities were improving again, or better, my feel and gut feeling got better again, and I had more confidence in the task at hand. But also, as Chris said, if I know it&rsquo;s going to be a hard task, I can do much more because I deliberately use AI for certain tasks to actually finish the task. So the future will tell.</p>
<h2 id="next-interview">Next Interview</h2>
<p>I hope you enjoyed this interview with Chris. Huge thanks to Chris for taking the time to speak with me and for sharing his experience with all of us. Follow him on <a href="https://www.linkedin.com/in/riccomini/" target="_blank" rel="noopener noreffer">LinkedIn</a>, <a href="https://x.com/criccomini" target="_blank" rel="noopener noreffer">X/Twitter</a> or on <a href="https://bsky.app/profile/chris.blue" target="_blank" rel="noopener noreffer">Bluesky</a>, read <a href="https://www.amazon.com/s?i=stripbooks&amp;rh=p_27%3AChris%2BRiccomini&amp;s=relevancerank&amp;text=Chris&#43;Riccomini" target="_blank" rel="noopener noreffer">his two amazing books</a>. Follow his amazing newsletter, the new one at <a href="https://rng.md/" target="_blank" rel="noopener noreffer">Posts on engineering, venture capital, AI, and more. | rng.md</a>, but also his old one <a href="https://materializedview.io/" target="_blank" rel="noopener noreffer">Materialized View</a> has a wealth of insights.</p>
<p>There are three more interviews already lined up with great guests, one of them is Wes McKinney as mentioned, so please share feedback, questions you might want to ask or just your experience on how to work with AI in the data space. We&rsquo;re all in this together, figuring it all out. The more we can learn from each other, what&rsquo;s important, and maybe also what&rsquo;s not, the better.</p>
<p>So stay tuned for the next interview.</p>
<hr>
<pre class=""><em>Full article published at <a href="https://motherduck.com/blog/cost-as-tokens-substrait-llm-chris-riccomini/" target="_blank" rel="noopener noreferrer">MotherDuck.com</a> - written as part of <a href="/services">my services</a></em></pre>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Chris hints at some of his recent <a href="https://rng.md/posts/wiggum-loop/" target="_blank" rel="noopener noreffer">Wiggum Loop</a> post.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>From <a href="https://wesmckinney.com/blog/agent-ergonomics/" target="_blank" rel="noopener noreffer">https://wesmckinney.com/blog/agent-ergonomics/</a>: Python will remain essential as an exploratory computing layer for humans and agents to collaborate on data analysis, research, and data visualization. Notebook layers (Jupyter, Marimo, and so forth) and hybrid IDEs (like Positron, where I’ve been contributing in the last couple of years) will increasingly focus on catering to the human-in-the-loop data scientist or ML engineer, even though the “Python part” may become thinner and thinner as the lower layers of the stack are re-engineered for performance and agentic engineering productivity.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
</item>
<item>
    <title>Internal vs. External Storage? What&#39;s the Limit of External Tables</title>
    <link>https://www.ssp.sh/blog/modern-external-tables-and-evolution/</link>
    <pubDate>Thu, 14 May 2026 00:08:08 &#43;0200</pubDate>
    <author>Simon Späti</author>
    <guid>https://www.ssp.sh/blog/modern-external-tables-and-evolution/</guid><enclosure url="https://www.ssp.sh/blog/modern-external-tables-and-evolution/featured-image.png" type="image/png" length="0" /><description><![CDATA[<p>When I started my career as a data warehouse engineer and business intelligence engineer in 2003, external tables with materialized views were the standard. We used external tables to integrate CSV files and other data not already in Oracle databases. Oracle External Tables have existed since 2001, and that&rsquo;s where I first used them. If the Lindy Effect continues to hold, we&rsquo;ll use external tables even longer. But why have they survived for so long?</p>
<p>The core question is: &ldquo;When should you store data internally in your warehouse versus externally in object storage?&rdquo;. Hot data queried frequently goes inside. Cold archival data stays external, where it&rsquo;s cheaper but slower. Interestingly, Databricks and BigQuery recently added external table features, but why? Not because they&rsquo;re trendy, but because the economics still work.</p>
<p>This article offers an inside look at external tables, their 25-year history, how they evolved from CSV parsers to ACID lakehouse tables, and whether you need to know about them today.</p>
<h2 id="what-are-external-tables">What Are External Tables?</h2>
<p>So what are external tables, and why have we been using them for so long? Why don&rsquo;t we just use the internal storage of a database?</p>
<p>In Oracle, where I first used them in 2008, they allowed you — and still do — to access data in external tables. External tables are defined as <strong>tables that do not reside in the database</strong>, and can be in any format for which an access driver<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> is provided. All of this is provided via <a href="https://en.wikipedia.org/wiki/Data_definition_language" target="_blank" rel="noopener noreffer">DDL</a> (Data Definition Language) of the database, describing an external table with all its columns, data types, etc., exposing the data as if it were residing in a regular database table.</p>
<p>The external data can be queried in parallel and <strong>queried directly using SQL</strong>. Essentially, it&rsquo;s read-only access to data stored outside of our database, making it available in a tabular, easy-to-work-with format to interact with existing tooling and language. In 2008, this was through procedural language such as PL-SQL in Oracle or T-SQL on MSSQL.</p>
<p>Today, external tables have evolved. The biggest change is that they can read more formats including semi-structured data such as Parquet, JSON, Avro, and ORC. While CSV was readable in 2008, the difference today is the columnar formats and nested formats that enable faster analytics. These are available for downstream processes and dashboards, but mostly accessed through SQL queries in one form or another.</p>
<p>A modern definition by <a href="https://research.google/pubs/biglake-bigquerys-evolution-toward-a-multi-cloud-lakehouse/" target="_blank" rel="noopener noreffer">BigLake</a>, an evolution of BigQuery toward a multi-cloud lakehouse that tries to solve key customer requirements around the unification of data lake and enterprise data warehousing workloads, <a href="https://docs.cloud.google.com/bigquery/docs/external-tables" target="_blank" rel="noopener noreffer">introducing</a> external tables in 2015 as part of it<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>:</p>
<blockquote>
<p>External tables are stored outside of BigQuery storage and refer to data that&rsquo;s stored outside of BigQuery. [..] Google Non-BigLake external tables let you query structured data in external data stores. To query a non-BigLake external table, you must have permissions to both the external table and the external data source.</p>
</blockquote>
<p>Snowflake <a href="https://docs.snowflake.com/en/sql-reference/sql/create-external-table" target="_blank" rel="noopener noreffer">defines</a> them as:</p>
<blockquote>
<p>[&hellip;]  When queried, an external table reads data from a set of one or more files in a specified external stage, and then outputs the data in a single VARIANT column. Additional columns can be defined, with each column definition consisting of a name, data type, and optionally whether the column requires a value (NOT NULL) or has any referential integrity constraints.</p>
</blockquote>
<p>External tables were <a href="https://www.snowflake.com/en/blog/external-tables-are-now-generally-available-on-snowflake/" target="_blank" rel="noopener noreffer">added in 2021</a>, and Snowflake described their benefits as follows:</p>
<blockquote>External Tables Address Key Data Lake Challenges:
<ol>
<li>To <strong>augment an existing data lake</strong>. [..] augment their existing data lake, rather than replace it. The External Tables feature enables that use case. Customers can use external tables to query the data in their data lake without ingesting it into Snowflake. (side note: MVs<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>)</li>
<li>Ad-hoc analytics. Customers often use external tables to <strong>run ad-hoc queries directly on raw data before ingesting the data</strong> into Snowflake. Ad-hoc queries help them evaluate data sets and determine further actions.</blockquote></li>
</ol>
<div class="mermaid" id="id-7"></div>
<h3 id="just-a-pointer-symlink">Just a Pointer (Symlink)?</h3>
<p>A simple analogy is a <strong>symlink in Linux</strong>, where you point from your current directory to another directory without moving data. You just add a pointer. If you read that file from that symlink, all it does is read it from the location the symlink points to.</p>
<p>An external table is the same, just a <strong>pointer</strong> to external data, bringing that data into the current data warehouse or cloud solution, hence the word external. You define the source format such as XML, CSV, etc., and define their structure, and then you can query that at any time. It&rsquo;s similar to a SQL View in that sense, but pointing to non-internal data.</p>
<p>Running <code>DROP TABLE</code> and deleting an external table is metadata-based only. No data is removed, only the table definition from the internal data catalog. The same is true with a symlink. Almost any relational database today has support for it, even if it&rsquo;s not called an external table. Everyone occasionally needs to read data outside of its warehouse or database.</p>
<h2 id="recap-in-the-history-of-external-tables">Recap in the History of External Tables</h2>
<p>Looking back at the history and evolution of external tables, we can quickly see that there&rsquo;s a long history and they&rsquo;ve been a <strong>recurring pattern</strong> across every generation of database technology since the early 2000s, and arguably longer if you count IBM&rsquo;s federated database concepts from the late 1990s.</p>
<div class="mermaid" id="id-8"></div>
<h3 id="the-origin-story-iso-in-2001">The Origin Story: ISO in 2001</h3>
<p>The history starts with <a href="https://www.iso.org/standard/31370.html" target="_blank" rel="noopener noreffer">ISO/IEC 9075-9</a>, published in 2001. Part 9 of the SQL standard defined foreign-data wrappers and datalink types for managing external data from within SQL. The work was completed in late 2000 and published alongside SQL:1999, with full integration in SQL:2003 (it was later <a href="https://www.iso.org/standard/84804.html" target="_blank" rel="noopener noreffer">updated in 2023</a>).</p>
<p>It was the initial definition and extensions to database language SQL to support management of external data <strong>through the use of foreign-data wrappers and datalink types</strong>.</p>
<p>My first encounter was with Oracle external tables, but according to <a href="https://en.wikipedia.org/wiki/Open_Database_Connectivity" target="_blank" rel="noopener noreffer">Wikipedia</a> there were earlier implementations, such as <strong>Microsoft Access linked tables (~1992)</strong>. Microsoft Access linked tables (~1992) were the earliest consumer-facing implementation where users could link dBASE, Paradox, text files, and ODBC sources as if they were Access tables. <strong>ODBC 1.0 (1992)</strong> itself established the first standard for heterogeneous data access across databases, though it didn&rsquo;t create table abstractions.</p>
<p>Further, <strong><a href="https://www.mcpressonline.com/analytics-cognitive/db2/the-as400-and-ibms-db2-datajoiner" target="_blank" rel="noopener noreffer">IBM&rsquo;s DB2 DataJoiner</a> (~1995)</strong> was more ambitious with a middleware product enabling SQL queries across Oracle, Sybase, SQL Server, Informix, Teradata, and even VSAM files through a unified interface. With <strong>SQL Server 7.0&rsquo;s Linked Servers (1998)</strong> we got federated querying to Microsoft&rsquo;s ecosystem via <strong>OLE DB</strong>, supporting cross-database joins with four-part naming conventions.</p>
<p>Most of these implementations shared a common limitation that Oracle (<a href="https://oracle-base.com/articles/9i/sql-new-features-9i" target="_blank" rel="noopener noreffer">9i Release 1 - 9.0.1</a> in 2001) solved: they focused on querying <em>other databases</em> or required middleware. Oracle&rsquo;s abstraction treated local flat files as first-class read-only table objects using the familiar <code>CREATE TABLE ... ORGANIZATION EXTERNAL</code> DDL syntax, providing a simple way to define external files as part of normal table creation and allowing ORACLE_LOADER access to query flat files (CSV, fixed-width, delimited) through DBAs.</p>
<p>It was an early way of separating declaration from compute (the Oracle loaders).</p>
<h2 id="why-external-tables-what-are-their-benefits">Why External Tables? What Are Their Benefits?</h2>
<p>But why use external tables? What makes them so useful that they persisted? Why have they <strong>survived so long</strong>, and why are they getting added to Databricks and other major platforms?</p>
<p>For that, we need to look at external tables&rsquo; benefits. The first reason is that external tables can simplify data access to <strong>avoid developing ETL pipelines</strong>, moving data out of the source, and re-ingesting it in our data warehouse. They make external data accessible easily, defined in a tabular form by a database schema with column types. Typical cloud data warehouses like Snowflake and Azure use them to link existing data from object storage easily without moving data. This makes the object storage files accessible for almost any downstream tool or query language in a simple and cost-effective way.</p>
<p>Other ways of using them are to store some data on <strong>cheaper storage</strong> (e.g., object storage over data warehouse storage) and only link them in. It&rsquo;s slower to fetch, but more affordable to keep. If you have large data sets, cost savings can be immense as this article <a href="https://medium.com/@abhidutty/optimize-data-storage-costs-by-70-using-databricks-snowflake-aws-s3-332f44949e93" target="_blank" rel="noopener noreffer">shows</a>, bringing down Snowflake internal storage cost from ~$23/TB/month to S3 infrequent access with ~$12.50/TB or S3 Glacier Deep Archive with only ~$1/TB.</p>
<p>Another handy side effect as the consumer of external table data is that the <strong>data is always up to date</strong>, because no refresh or update is needed. It goes without saying that this has its own downsides and can be a problem for the owner of the data if it&rsquo;s used in production and the ETL process reads large amounts of data through external tables. This will affect upstream apps running or owning this data.</p>
<p>That&rsquo;s why many use external tables in combination with materialized views (MVs) to truncate and recreate a daily snapshot (or similar) during off-peak (mostly nights) of this data, avoiding affecting production data and even optimizing query performance with added indices for downstream queries.</p>
<h3 id="when-internal-and-when-external-data-whats-the-limit-of-external">When Internal and When External Data? What&rsquo;s the Limit of External?</h3>
<p>The tradeoffs come down to how often the data is queried, e.g. the hot versus cold question.</p>
<p>The tradeoffs and considerations you should make when wanting to use them come down to the decision of how often the data is queried. The table below shows it in more detail:</p>
<table>
  <thead>
      <tr>
          <th>Dimension</th>
          <th><strong>Internal Storage</strong></th>
          <th><strong>External Tables</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Temperature</td>
          <td><strong>Hot</strong>: recent data, lasts weeks to months</td>
          <td><strong>Cold</strong>: archival or infrequently touched</td>
      </tr>
      <tr>
          <td>Typical use case</td>
          <td>Dashboards, frequent queries, sub-second latency</td>
          <td>Archival, ad-hoc exploration, augmenting a data lake</td>
      </tr>
      <tr>
          <td>Query speed</td>
          <td>Fast, optimized for repeated access</td>
          <td>Slower (a 1.3×–1.7× tax in the below dashboard benchmark)</td>
      </tr>
      <tr>
          <td>Storage cost</td>
          <td>Higher (warehouse-managed, ~$23/TB on Snowflake capacity)</td>
          <td>Lower: up to ca. 20× cheaper on S3 Glacier Deep Archive (~$1/TB)</td>
      </tr>
      <tr>
          <td>Data freshness</td>
          <td>Can go stale between ETL refreshes</td>
          <td>Always up to date, no refresh needed</td>
      </tr>
      <tr>
          <td>Setup effort</td>
          <td>Requires ETL pipelines, scripts or re-ingestion</td>
          <td>Simple DDL-only definition, data stays in place</td>
      </tr>
      <tr>
          <td>Scaling concern</td>
          <td>Disk grows faster than compute needs</td>
          <td>Heavy reads can affect upstream apps owning the source files</td>
      </tr>
      <tr>
          <td>Operational overhead</td>
          <td>Predictable, managed by the warehouse</td>
          <td>Small-file problem and manifest management for tiny or streaming datasets</td>
      </tr>
  </tbody>
</table>
<p>In the era of data lake and lakehouse architectures, this is an important consideration. VSCO <a href="https://eng.vsco.co/querying-s3-data-with-redshift-spectrum/" target="_blank" rel="noopener noreffer">says</a>: &ldquo;disk space was growing more quickly than our compute needs,&rdquo; which is what triggered the adoption of external tables.</p>
<p>If you look at your use case, if you need to do analytics across various sources with joins and augmentation of your data at an enterprise, you probably want to focus on loading data into your database or data warehouse, an architectural pattern that has survived more than 30 years. But if you have data that is external and small but you want to join it with existing data, or you always need fresh data and can live with a slower response time (maybe because it runs during the night), you might use external tables.</p>
<p>In any case, external tables are a good approach to keep in mind and a valuable <a href="https://motherduck.com/blog/data-engineering-toolkit-essential-tools/" target="_blank" rel="noopener noreffer">toolkit</a> to have.</p>
<h3 id="they-work-well-with-existing-tech-and-common-patterns">They Work Well with Existing Tech and Common Patterns</h3>
<p>Obviously, today&rsquo;s external tables are not the same as the earliest ones in Microsoft Access, but the principle of accessing data outside your system is still the same. Nowadays we have more support, new formats besides CSV and JSON. We can do Parquet or open table formats.</p>
<p>As mentioned, they work well with related long-lasting data warehouse patterns and applications such as materialized views and stored procedures. The recurring pattern is to access external data with your data management system, similar to the pattern of materialized views that refresh complex SQL statements and make them fast, and stored procedures that run glue code within your database.</p>
<p>Moreover, there are temporary tables that are similar but only available during a transaction or session. They all work in the same Lindy effect, e.g., Databricks just <a href="https://www.databricks.com/blog/introducing-temporary-tables-databricks-sql" target="_blank" rel="noopener noreffer">announced Temporary table support</a> recently on December 9th, 2025, or Databricks SQL Stored Procedure a <a href="https://www.databricks.com/blog/introducing-sql-stored-procedures-databricks" target="_blank" rel="noopener noreffer">little earlier</a>, August 14th, 2025, for reusing existing SQL statements.</p>
<p>Again and again, <strong>everything that is old will be new again</strong>. Exactly what the Lindy Effect is all about. We can clearly say that the Lindy effect over the last 33 years applies here. The longer something is in place, the more likely it is to be around for at least that long.</p>
<blockquote>
<p>[!info] External vs. Temporary Table</p>
<p>In contrast: temp table = session-scoped, writable, fast, invisible to others, auto-dropped. External table = persistent metadata, read-only, infinite size, visible to all, optimized for cost.</p>
<p>A common chain in practice is going from: <code>external table → temp/transient table → permanent managed table</code>.</p>
</blockquote>
<h3 id="how-a-classical-external-table-works">How a Classical External Table Works</h3>
<p>To understand how traditional external tables work, let&rsquo;s first look at Oracle, which has built an extensive syntax around them and where they still work this way today.</p>
<p>First, we can create a place for external data called <code>DIRECTORIES</code>, which is simply a pointer or alias to a file system location where external files already exist:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">OR</span><span class="w"> </span><span class="k">REPLACE</span><span class="w"> </span><span class="n">DIRECTORY</span><span class="w"> </span><span class="n">admin_dat_dir</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">AS</span><span class="w"> </span><span class="s1">&#39;/flatfiles/data&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>This directory can point to local file systems, NFS mounts, or even cloud object storage today (with the <code>ORACLE_BIGDATA</code> driver for S3, OCI, Azure). The <code>DIRECTORIES</code> don&rsquo;t require moving data, though you could prepare those files via ETL pipelines or third-party tools, or they can be generated directly by applications.</p>
<p>We can now create an external table based on this directory, e.g., log files, bad data that we store externally, JSON files, and make data accessible inside the <a href="https://en.wikipedia.org/wiki/Information_schema" target="_blank" rel="noopener noreffer">INFORMATION_SCHEMA</a> and with plain SQL, as if it were internal.</p>
<p>Creating an external table:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">admin_ext_employees</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                   </span><span class="p">(</span><span class="n">employee_id</span><span class="w">       </span><span class="nb">NUMBER</span><span class="p">(</span><span class="mi">4</span><span class="p">),</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">first_name</span><span class="w">        </span><span class="n">VARCHAR2</span><span class="p">(</span><span class="mi">20</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">last_name</span><span class="w">         </span><span class="n">VARCHAR2</span><span class="p">(</span><span class="mi">25</span><span class="p">),</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">job_id</span><span class="w">            </span><span class="n">VARCHAR2</span><span class="p">(</span><span class="mi">10</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">manager_id</span><span class="w">        </span><span class="nb">NUMBER</span><span class="p">(</span><span class="mi">4</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">hire_date</span><span class="w">         </span><span class="nb">DATE</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">salary</span><span class="w">            </span><span class="nb">NUMBER</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span><span class="mi">2</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">commission_pct</span><span class="w">    </span><span class="nb">NUMBER</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span><span class="mi">2</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">department_id</span><span class="w">     </span><span class="nb">NUMBER</span><span class="p">(</span><span class="mi">4</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">email</span><span class="w">             </span><span class="n">VARCHAR2</span><span class="p">(</span><span class="mi">25</span><span class="p">)</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">                   </span><span class="p">)</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="n">ORGANIZATION</span><span class="w"> </span><span class="k">EXTERNAL</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="p">(</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="k">TYPE</span><span class="w"> </span><span class="n">ORACLE_LOADER</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="k">DEFAULT</span><span class="w"> </span><span class="n">DIRECTORY</span><span class="w"> </span><span class="n">admin_dat_dir</span><span class="w">  </span><span class="c1">--notice this dir with above
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="k">ACCESS</span><span class="w"> </span><span class="k">PARAMETERS</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="p">(</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="n">records</span><span class="w"> </span><span class="n">delimited</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="n">newline</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="n">badfile</span><span class="w"> </span><span class="n">admin_bad_dir</span><span class="p">:</span><span class="s1">&#39;empxt%a_%p.bad&#39;</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="n">logfile</span><span class="w"> </span><span class="n">admin_log_dir</span><span class="p">:</span><span class="s1">&#39;empxt%a_%p.log&#39;</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="n">fields</span><span class="w"> </span><span class="n">terminated</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="s1">&#39;,&#39;</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="n">missing</span><span class="w"> </span><span class="n">field</span><span class="w"> </span><span class="k">values</span><span class="w"> </span><span class="k">are</span><span class="w"> </span><span class="k">null</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="p">(</span><span class="w"> </span><span class="n">employee_id</span><span class="p">,</span><span class="w"> </span><span class="n">first_name</span><span class="p">,</span><span class="w"> </span><span class="n">last_name</span><span class="p">,</span><span class="w"> </span><span class="n">job_id</span><span class="p">,</span><span class="w"> </span><span class="n">manager_id</span><span class="p">,</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">           </span><span class="n">hire_date</span><span class="w"> </span><span class="nb">char</span><span class="w"> </span><span class="n">date_format</span><span class="w"> </span><span class="nb">date</span><span class="w"> </span><span class="n">mask</span><span class="w"> </span><span class="s2">&#34;dd-mon-yyyy&#34;</span><span class="p">,</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">           </span><span class="n">salary</span><span class="p">,</span><span class="w"> </span><span class="n">commission_pct</span><span class="p">,</span><span class="w"> </span><span class="n">department_id</span><span class="p">,</span><span class="w"> </span><span class="n">email</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="p">)</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="p">)</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="k">LOCATION</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;empxt1.dat&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;empxt2.dat&#39;</span><span class="p">)</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="p">)</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="n">PARALLEL</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="n">REJECT</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="n">UNLIMITED</span><span class="p">;</span><span class="w"> 
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>The first and most important choice is <code>TYPE</code>, which determines the access driver and what kind of files you can read: <code>ORACLE_LOADER</code> for plain text files like CSV or logs (read-only), <code>ORACLE_DATAPUMP</code> for Oracle binary dump files, <code>ORACLE_BIGDATA</code> for cloud object stores like S3 or OCI in formats like Parquet or Avro, and <code>ORACLE_HIVE</code> for Hadoop/Hive data. The <code>DEFAULT DIRECTORY</code> points to a server-side path alias, and <code>LOCATION</code> names the actual file(s), with wildcard support (<code>*.dat</code>) so you can load a whole batch at once.</p>
<p>The <code>ACCESS PARAMETERS</code> block is where you control parsing: row and field delimiters, null handling, custom date format masks, and where to write bad rows (<code>badfile</code>) and parse logs (<code>logfile</code>). On top of that, <code>PARALLEL</code> lets Oracle split file reading across multiple processes for large files, and <code>REJECT LIMIT</code> controls fault tolerance. Set it to <code>UNLIMITED</code> to skip bad rows silently, or <code>0</code> to fail immediately on the first error.</p>
<p>You see lots of built-in features that we can use compared to building a full-fledged data pipeline. Instead of exporting and importing CSVs from the source databases or developing a complex CDC pipeline that traditionally looked something like: <code>source OLTP --&gt; CSVs --&gt; IDW (reports on yesterday) -&gt; ingest into DWH for long-term analytics</code>, we can just define a table based on external data and access it as part of our pipeline.</p>
<blockquote>
<p>[!tip] The INFORMATION_SCHEMA analogy</p>
<p>You are probably familiar with the INFORMATION_SCHEMA of a database. It&rsquo;s the <strong>internal data catalog</strong> that most databases provide and it contains a <strong>list of all tables and all metadata</strong> such as columns, data types, etc. The neat thing is that external tables will show up as internal tables once defined.</p>
</blockquote>
<h2 id="whats-the-modern-version-of-external-tables-today">What&rsquo;s the Modern Version of External Tables Today?</h2>
<p>To preface: the previous Oracle example shows the <code>CREATE EXTERNAL TABLE</code> syntax, and a first-class DDL object in the data catalog. What follows in this chapter is the next evolution, where external tables are not necessarily created with DDL, but in another way, achieving the same outcome of querying data in place without loading it. Let&rsquo;s see what these are.</p>
<h3 id="integrated-into-warehouses">Integrated into Warehouses</h3>
<p>Most modern warehouses - Snowflake, Redshift Spectrum, BigQuery, Athena, Synapse - come with a simplified version of <code>CREATE EXTERNAL TABLE</code>. Compared to the Oracle example, the schema is usually inferred from the file format (especially Parquet), S3 or another object store is the default backing location, and the parsing ceremony disappears. The pseudo-code looks roughly like this across engines:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Pseudo-code: modern external table over Parquet on S3
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">EXTERNAL</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">sales</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WITH</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">LOCATION</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;s3://my-bucket/sales/&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">FORMAT</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;PARQUET&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">);</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Object storage like S3, GCS, and Azure Blob has become the first-class citizen for external data. From here, the ecosystem layers on: dbt wraps this in YAML, DuckDB skips the DDL entirely in favor of schema-on-read, and open table formats add transactional guarantees on top.</p>
<h3 id="external-tables-with-dbt">External Tables with dbt?</h3>
<p>On top of this base SQL form, dbt adds a YAML layer and can be used with its own package called <a href="https://github.com/dbt-labs/dbt-external-tables" target="_blank" rel="noopener noreffer"><code>dbt-external-tables</code></a>. It&rsquo;s one of the most-used dbt packages, though it seems less actively maintained now.</p>
<p>The external table is defined via YAML, and there are lots of options to set, with the most important being <code>external</code> and its <code>location</code>, but also defining <code>columns</code> in different ways such as inference or the <code>meta</code> tag:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">sources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">snowplow</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">tables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">event</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="p">&gt;</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">            This source table is actually a set of files in external storage.
</span></span></span><span class="line"><span class="cl"><span class="sd">            The dbt-external-tables package provides handy macros for getting
</span></span></span><span class="line"><span class="cl"><span class="sd">            those files queryable, just in time for modeling.
</span></span></span><span class="line"><span class="cl"><span class="sd">                            </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">external</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">location:         # required</span><span class="p">:</span><span class="w"> </span><span class="l">S3 file path, GCS file path, Snowflake stage, Synapse data source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="l">...              </span><span class="w"> </span><span class="c"># database-specific properties of external table</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">partitions</span><span class="p">:</span><span class="w">       </span><span class="c"># optional</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">collector_date</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">data_type</span><span class="p">:</span><span class="w"> </span><span class="l">date</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="l">...          </span><span class="w"> </span><span class="c"># database-specific properties</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># Specify ALL column names + datatypes.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># Column order must match for CSVs, column names must match for other formats.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># Some databases support schema inference.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">columns</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app_id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">data_type</span><span class="p">:</span><span class="w"> </span><span class="l">varchar(255)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Application ID&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">platform</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">data_type</span><span class="p">:</span><span class="w"> </span><span class="l">varchar(255)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Platform&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="l">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># Use `meta` to pass custom column properties (e.g. alias, expression)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">columns</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">raw_timestamp</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">data_type</span><span class="p">:</span><span class="w"> </span><span class="l">timestamp</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">meta</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">alias</span><span class="p">:</span><span class="w"> </span><span class="l">event_timestamp      </span><span class="w"> </span><span class="c"># rename the column in the external table</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">expression</span><span class="p">:</span><span class="w"> </span><span class="l">TO_TIMESTAMP(...)</span><span class="w"> </span><span class="c"># custom SQL expression instead of default value extraction</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>This is a nice improvement over the ODBC GUI interface. It&rsquo;s not exactly an apples-to-apples comparison as dbt itself is not a database, but with its supported destinations such as Redshift (Spectrum), Snowflake, BigQuery, Spark, Synapse, and Azure SQL, you see that it will persist in these destinations, mostly data warehouses.</p>
<h3 id="duckdb-with-dbt">DuckDB with dbt</h3>
<p>If you use dbt, you can also use DuckDB with dbt via <a href="https://github.com/duckdb/dbt-duckdb" target="_blank" rel="noopener noreffer">dbt-duckdb</a>, which is more up-to-date. But DuckDB is not an external table, right?</p>
<p>Yes, DuckDB doesn&rsquo;t have <code>CREATE EXTERNAL TABLE</code> syntax <a href="https://github.com/duckdb/duckdb/discussions/14422" target="_blank" rel="noopener noreffer">yet</a>, mostly because it is an in-memory database, but you can achieve the same functionality through other means. DuckDB can not only be used as a database but also as a zero-copy SQL connector (see all categories at <a href="/blog/enterprise-case-duckdb-key-categories/" rel="">5 Key Categories</a>). We can just point it to an external source, as shown above with dbt. The difference is that DuckDB is both a database and a compute engine, making ad-hoc reads possible directly without a DDL definition, similar to an external table with Oracle loaders. With dbt, we can nicely declare this in dbt configs.</p>
<p>With DuckDB, you can query &ldquo;external data&rdquo; extremely fast over HTTPS or locally in formats such as Parquet, CSV, and <a href="https://duckdb.org/docs/current/data/data_sources" target="_blank" rel="noopener noreffer">many more</a>, so the need for formal external tables is reduced since DuckDB does <strong>schema on read</strong>.</p>
<p>If you want to define the database schema ahead of time, we&rsquo;d use external tables to do that and effectively have <strong>schema on write</strong> (though we don&rsquo;t write, just define the DDL table structure and data types), which is more of the classical ETL approach.</p>
<p>Here&rsquo;s an example with <code>external_location</code> to read external data with dbt:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">sources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">external_source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">external_location</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;s3://my-bucket/my-sources/{name}.parquet&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">tables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">source1</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Read more at <a href="https://duckdb.org/2025/04/04/dbt-duckdb" target="_blank" rel="noopener noreffer">Fully Local Data Transformation with dbt and DuckDB</a>.</p>
<p>Other options are with database views that are supported in DuckDB with <strong><code>CREATE VIEW</code> over <code>read_parquet()</code></strong>. You can ship a .duckdb file to clients with pre-defined views over S3 data, so clients don&rsquo;t need to know about the underlying data, Hive partitioning, or even glob patterns — very similar to what a formal <code>CREATE EXTERNAL TABLE</code> would do.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">VIEW</span><span class="w"> </span><span class="n">events</span><span class="w"> </span><span class="k">AS</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">read_parquet</span><span class="p">(</span><span class="s1">&#39;s3://lake/events/*.parquet&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">hive_partitioning</span><span class="o">=</span><span class="k">true</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Or similarly use <code>ATTACH</code> to directly point to Postgres, MySQL, SQLite, S3, and others:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Postgres (binary wire protocol, predicate + projection pushdown, read+write)
</span></span></span><span class="line"><span class="cl"><span class="n">INSTALL</span><span class="w"> </span><span class="n">postgres</span><span class="p">;</span><span class="w"> </span><span class="k">LOAD</span><span class="w"> </span><span class="n">postgres</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">ATTACH</span><span class="w"> </span><span class="s1">&#39;dbname=postgres user=postgres host=127.0.0.1&#39;</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">pg</span><span class="w"> </span><span class="p">(</span><span class="k">TYPE</span><span class="w"> </span><span class="n">postgres</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">ATTACH</span><span class="w"> </span><span class="s1">&#39;postgresql://user@host/db&#39;</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">pg</span><span class="w"> </span><span class="p">(</span><span class="k">TYPE</span><span class="w"> </span><span class="n">postgres</span><span class="p">,</span><span class="w"> </span><span class="n">READ_ONLY</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- MySQL (via MariaDB Connector/C; Postgres-style keyvalue string even for MySQL — easy trap)
</span></span></span><span class="line"><span class="cl"><span class="n">INSTALL</span><span class="w"> </span><span class="n">mysql</span><span class="p">;</span><span class="w"> </span><span class="k">LOAD</span><span class="w"> </span><span class="n">mysql</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">ATTACH</span><span class="w"> </span><span class="s1">&#39;host=localhost user=root port=0 database=mysql&#39;</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">mdb</span><span class="w"> </span><span class="p">(</span><span class="k">TYPE</span><span class="w"> </span><span class="n">mysql</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- SQLite (file opens directly; multi-reader single-writer by SQLite file locks)
</span></span></span><span class="line"><span class="cl"><span class="n">INSTALL</span><span class="w"> </span><span class="n">sqlite</span><span class="p">;</span><span class="w"> </span><span class="k">LOAD</span><span class="w"> </span><span class="n">sqlite</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">ATTACH</span><span class="w"> </span><span class="s1">&#39;sakila.db&#39;</span><span class="w"> </span><span class="p">(</span><span class="k">TYPE</span><span class="w"> </span><span class="n">sqlite</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- Generic remote DuckDB file
</span></span></span><span class="line"><span class="cl"><span class="n">ATTACH</span><span class="w"> </span><span class="s1">&#39;s3://duckdb-blobs/databases/stations.duckdb&#39;</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">stations_db</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h3 id="open-table-formats-and-lakehouse-architecture">Open Table Formats and Lakehouse Architecture</h3>
<p>That begs the question of whether <a href="https://motherduck.com/blog/open-lakehouse-stack-duckdb-table-formats/" target="_blank" rel="noopener noreffer">Open Table Formats</a> are the next evolution and modern way of external tables. These table formats allow almost any SQL compute engine to use them as external tables, and read, compute, and aggregate as a database would.</p>
<p>If we look at what table formats consist of, they&rsquo;re built on object storage, with a file format like Parquet, and then we have a manifest file that contains a list of files that <strong>unifies multiple single files into a &ldquo;single&rdquo; table</strong>, looking from the outside.</p>
<p>So again, the manifest file is our pointer or fancier symlink, but it lives next to the data, unlike external tables. There&rsquo;s much more going on in table formats, but if we have a <strong>data lake with open table format tables</strong>, we can see how we define tables in DDL and the <strong>pointers are to different files</strong> (Parquet, ORC, Avro), in most cases Parquet.</p>
<p>More broadly, we can say external tables decouple storage from compute. Open table formats decouple the table itself (schema, history, transactions, statistics) from any single engine.</p>
<h3 id="lakehouse-and-connecting-to-ducklake">Lakehouse and Connecting to DuckLake</h3>
<p>One step further is obviously a lakehouse architecture, with the shift from <em>format-agnostic file reading</em> to <em>governed, transactional, multi-engine open table formats</em>.</p>
<p>If you extend the external table idea to a <a href="https://motherduck.com/blog/from-data-lake-to-lakehouse-duckdb-portable-catalog/" target="_blank" rel="noopener noreffer">lakehouse architecture</a>, these external tables with open table formats provide essentially what databases provide with ACID guarantees, time travel, schema evolution, partition evolution, and fine-grained access control, but for files.</p>
<p>But with the difference that data stays in open Parquet file format on customer-owned cloud storage. The external table, once a humble workaround for avoiding data loads, has become the architectural foundation of the data lakehouse if you like this analogy.</p>
<p>With <a href="https://ducklake.select/" target="_blank" rel="noopener noreffer">DuckLake</a>, we have the next evolution just around the corner, bringing back exactly that missing database, especially to handle all the metadata of such a lakehouse and all its files. This means having durable and consistent database storage for our <a href="https://iceberg.apache.org/spec/#manifests" target="_blank" rel="noopener noreffer">manifest files</a>.</p>
<h4 id="open-data-catalog-to-complete-the-picture-the-odbc-glue">Open Data Catalog to Complete the Picture: The ODBC Glue</h4>
<p>With all these evolutions, we&rsquo;ve come far. When adding an <a href="https://www.ssp.sh/brain/open-table-format-catalogs" target="_blank" rel="noopener noreffer">Open Data Catalog</a>, we are exactly where we started: having an INFORMATION_SCHEMA, a dictionary with all our tables, in this case the open table format tables.</p>
<p>It&rsquo;s the <strong>glue that ODBC provided when connecting a BI tool to the underlying database</strong>. Now you&rsquo;d like to have an open data catalog that, in the best-case scenario, gives you all the tables and ways to connect.</p>
<p>But then again, the syntax of <code>EXTERNAL TABLES</code> still gets added, and <a href="https://arrow.apache.org/docs/format/ADBC.html" target="_blank" rel="noopener noreffer">ADBC</a> and DuckDB are doing a great job of using external data without needing a data lake and its technology stack altogether. For example, DuckDB has support for <a href="https://duckdb.org/docs/current/core_extensions/odbc/overview" target="_blank" rel="noopener noreffer">ODBC</a>, <a href="https://duckdb.org/docs/current/clients/adbc" target="_blank" rel="noopener noreffer">ADBC</a> and even <a href="https://duckdb.org/docs/current/clients/java" target="_blank" rel="noopener noreffer">JDBC</a>. That matters especially for 3rd-party tools: ADBC streams Apache Arrow end-to-end instead of serializing row-by-row, so BI tools and notebooks can pull millions of rows directly from external Parquet tables at speeds that previously required keeping data &ldquo;hot&rdquo; in a cloud data warehouse. 🙂</p>
<blockquote>
<p>[!note] ADBC, what is that?<br>
ODBC is 30+ years old, and we have a newer, faster version of it, called <a href="https://arrow.apache.org/docs/format/ADBC.html" target="_blank" rel="noopener noreffer">ADBC</a>. It&rsquo;s a faster way to connect to other databases with a columnar-oriented API instead of <strong>row-by-row serialization</strong>, heavily making use of Apache Arrow.</p>
<p>While ADBC is newer, it tries to support the same drivers as ODBC, but faster and easier to install. E.g., it has a handy <a href="https://github.com/columnar-tech/dbc" target="_blank" rel="noopener noreffer">dbc</a> CLI to install it on almost any programming language, so no more manual and error-prone Windows GUI ODBC downloading of drivers and definitions needed, just one CLI command.</p>
</blockquote>
<blockquote>
<p>[!tip] Using MotherDuck<br>
If you want a data warehouse that just works, integrates well with DuckDB, and has support for DuckLake, you can always use managed MotherDuck. You can build a classical data warehouse with plain SQL, you can read external data easily with DuckDB or dbt-duckdb, or <a href="https://motherduck.com/blog/announcing-ducklake-1-0-on-motherduck/" target="_blank" rel="noopener noreffer">integrate with DuckLake</a>.</p>
<p>It works great <a href="https://motherduck.com/blog/motherduck-agent-skills/" target="_blank" rel="noopener noreffer">with agents</a>. Check out MotherDuck&rsquo;s <a href="https://github.com/motherduckdb/agent-skills/" target="_blank" rel="noopener noreffer">agent-skills</a> for opinionated AI skills for building applications with MotherDuck. And <a href="https://motherduck.com/product/dives/" target="_blank" rel="noopener noreffer">visualize with Dives</a> with one prompt.</p>
</blockquote>
<h2 id="which-is-faster-a-quick-benchmark">Which Is Faster? A Quick Benchmark</h2>
<p>To put numbers behind the hot/cold decision, I ran a simple benchmark on the TPC-H SF=1 <code>lineitem</code> table (6M rows, ~150 MB), stored four ways: inside a DuckDB file (internal), as raw Parquet, as an Iceberg table, and as a DuckLake table. Full code: <a href="https://github.com/sspaeti/external-table-benchmark/blob/main/bench2.py" target="_blank" rel="noopener noreffer"><code>bench2.py</code></a> and <a href="https://github.com/sspaeti/external-table-benchmark/blob/main/metadata_bench.py" target="_blank" rel="noopener noreffer"><code>metadata_bench.py</code></a>.</p>
<p><strong>Dashboard workload (hot path)</strong>: 3 queries × 10 repeats:</p>
<table>
  <thead>
      <tr>
          <th>Backend</th>
          <th>Tier</th>
          <th>Median</th>
          <th>p95</th>
          <th>vs internal</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Internal (DuckDB)</td>
          <td>hot</td>
          <td>23.8 ms</td>
          <td>235 ms</td>
          <td><strong>1.0×</strong></td>
      </tr>
      <tr>
          <td>DuckLake</td>
          <td>cold</td>
          <td>45.1 ms</td>
          <td>269 ms</td>
          <td>1.3×</td>
      </tr>
      <tr>
          <td>External Parquet</td>
          <td>cold</td>
          <td>41.3 ms</td>
          <td>271 ms</td>
          <td>1.4×</td>
      </tr>
      <tr>
          <td>External Iceberg</td>
          <td>cold</td>
          <td>56.1 ms</td>
          <td>377 ms</td>
          <td>1.7×</td>
      </tr>
  </tbody>
</table>
<p>Internal is fastest; external pays a 1.3×–1.7× tax. But for <strong>cold/archival queries</strong> (one-off, no warmup), all four backends answered in under 150 ms. The speed difference effectively vanishes for data you query once a week.</p>
<p><strong>Storage cost</strong> is where external tables shine. Columnar Parquet is ~40% smaller than native DuckDB format. Ten TB of archive data costs roughly ~$125/month on S3 Infrequent Access or ~$10/month on Glacier Deep Archive, versus ~$230/month inside Snowflake on capacity pricing. This is the economic case external tables were invented for, and it still holds.</p>
<p><strong>Metadata workload</strong> is where DuckLake stands out. Fifty single-row inserts showed DuckLake creating <strong>zero data files</strong> (rows inlined in the catalog) versus Iceberg&rsquo;s <strong>352 files</strong> (201 data + 151 metadata). That&rsquo;s the &ldquo;small file problem&rdquo; made concrete: at one write per second, Iceberg creates ~86,400 files per day needing compaction. DuckLake creates zero until you checkpoint. DuckDB Labs&rsquo; own benchmarks report up to <a href="https://ducklake.select/2026/04/02/data-inlining-in-ducklake/" target="_blank" rel="noopener noreffer">926× faster queries</a> on streaming workloads.</p>
<h2 id="so-should-you-use-external-tables">So Should You Use External Tables?</h2>
<p>So after all this, should you use external tables today? After seeing how sticky they&rsquo;ve been since Oracle 9i in 2001, how they keep getting re-added to newer tools (Snowflake in 2021, Databricks Unity Catalog, BigLake in 2022), and how their core benefit is. Accessing data where it lives without moving it, via a simple DDL statement, has only grown more valuable as formats have evolved from CSV to Parquet, JSON, Avro, and now open table formats. I&rsquo;d say yes. But choose wisely based on your data&rsquo;s temperature: use internal storage for hot data, such as dashboards and frequently used queries.</p>
<p>Use external tables for cold data, archival workloads, and ad-hoc exploration, where that gap vanishes, and storage costs plummet (up to 20× cheaper on Glacier Deep Archive vs. warehouse-managed storage). And if you already use dbt, DuckDB, or a lakehouse stack, the modern versions are right there. Where they&rsquo;re the <em>wrong</em> choice is the inverse: transactional workloads, queries that need sub-second latency on every run, or data so small that the operational overhead of an external stage outweighs the benefit of not loading it.</p>
<p>The evolution is worth naming explicitly: &ldquo;read CSVs on disk&rdquo; → &ldquo;read Parquet on HDFS&rdquo; → &ldquo;read Parquet on S3 via a metastore&rdquo; → &ldquo;read Iceberg/Delta tables with ACID on S3&rdquo; → &ldquo;the Iceberg table <em>is</em> the warehouse table&rdquo;. Each step kept the core idea (data stays where it lives, metadata describes it, SQL queries it) and added database semantics back in. With open data catalogs, the warehouse becomes a <strong>stateless rental over a bucket you own</strong>, and external tables are increasingly managed. DuckLake demonstrates this best: when the catalog has SQL-DB-like guarantees, the distinction between &ldquo;external&rdquo; and &ldquo;internal&rdquo; dissolves. The metadata benchmark made this concrete by reading a single indexed row rather than walking a manifest tree.</p>
<p>The <strong>database semantics are returning</strong> with DuckLake, managed Iceberg, and predictive optimization, all of which reintroduce RDBMS-style guarantees to the lake. The cycle from &ldquo;external table for cheap storage&rdquo; to &ldquo;external table as a full ACID database on S3&rdquo; took 25 years, completing the journey back to database principles while maintaining the separation of storage and compute. You can say <strong>the modern external table isn&rsquo;t external anymore</strong>. DuckDB reads them directly, and DuckLake handles the metadata that multifile lakehouse architectures would otherwise drown in. The lesson from history is that whenever someone tries to replace it, the pattern is that reading data in place always beats moving it. And the Lindy Effect suggests that if external tables have lasted 25 years and get re-added, they&rsquo;ll persist another 25. They&rsquo;re probably not going anywhere. 🙂</p>
<hr>
<pre class=""><em>Full article published at <a href="https://motherduck.com/blog/internal-vs-external-storage-whats-the-limit-of-external-tables/" target="_blank" rel="noopener noreferrer">MotherDuck.com</a> - written as part of <a href="/services">my services</a></em></pre>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>A so-called loader that lets you access the data via a driver: see the ORACLE_LOADER Access Driver example: <a href="https://docs.oracle.com/en/database/oracle/oracle-database/12.2/sutil/oracle_loader-access-driver.html" target="_blank" rel="noopener noreffer">https://docs.oracle.com/en/database/oracle/oracle-database/12.2/sutil/oracle_loader-access-driver.html</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Also see the latest release notes of BigQuery from April 2026; lots of it has to do with &ldquo;external catalogs&rdquo; and also BigQuery Apache Iceberg external tables now support Iceberg version 3: <a href="https://docs.cloud.google.com/bigquery/docs/release-notes#April_21_2026" target="_blank" rel="noopener noreffer">https://docs.cloud.google.com/bigquery/docs/release-notes#April_21_2026</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Customers can also choose to create materialized views on external tables to speed up the query performance significantly.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
</item>
</channel>
</rss>
