<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[anthu.dev]]></title><description><![CDATA[Real problems, working code.]]></description><link>https://anthu.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 07 Apr 2026 21:41:57 GMT</lastBuildDate><atom:link href="https://anthu.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Managing Snowflake organization listings from dbt]]></title><description><![CDATA[Most teams I talk to already treat dbt as the place where tables and views get built, tested, and deployed. The awkward bit shows up right after that: Internal and External Marketplace listings — shar]]></description><link>https://anthu.dev/managing-snowflake-organization-listings-from-dbt</link><guid isPermaLink="true">https://anthu.dev/managing-snowflake-organization-listings-from-dbt</guid><category><![CDATA[dbt]]></category><category><![CDATA[snowflake]]></category><category><![CDATA[Data Mesh]]></category><dc:creator><![CDATA[Anton Huck]]></dc:creator><pubDate>Fri, 03 Apr 2026 01:36:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69664db126b81fb851c48b13/1d57faa3-f64c-413f-99ba-caefe777b6cf.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most teams I talk to already treat dbt as the place where tables and views get built, tested, and deployed. The awkward bit shows up right after that: <strong>Internal and External Marketplace</strong> listings — shares, grants, manifests, publish vs draft — often live in one-off SQL scripts, runbooks, or someone’s Snowsight tabs. The warehouse is versioned; the listing story is not.</p>
<p>I’ve been experimenting with a small dbt package that tries to close that gap: <a href="https://github.com/anthu/dbt-snowflake-listings"><strong>dbt-snowflake-listings</strong></a> (<code>anthu/dbt_snowflake_listings</code> on the hub of your <code>packages.yml</code>). The idea is simple: <strong>a listing is just another dbt model</strong> with a custom materialization. <code>dbt run</code> creates or alters the share, applies grants, and syncs the listing manifest — still wired into the DAG via ordinary <code>ref()</code> dependencies.</p>
<p>Fair warning up front: this is <strong>highly experimental</strong>. APIs and Snowflake listing behavior evolve; I’m dogfooding it on real projects, but I would not bet a compliance audit on “set and forget” without your own testing. Think of it as a opinionated spike that happened to grow tests and docs — not a supported product.</p>
<hr />
<h2>Why bother with listings-as-code?</h2>
<p>Organization listings sit on top of shares and metadata. Doing that by hand works until:</p>
<ul>
<li><p>You need the <strong>same objects</strong> you already model in dbt to appear in the share, in lockstep with builds.</p>
</li>
<li><p>You want <strong>idempotent</strong> updates (rerun deploy → alter listing, re-grant) instead of duplicate “create listing” scripts.</p>
</li>
<li><p>You care about <strong>reviewability</strong>: manifest YAML in Git next to the models consumers actually see.</p>
</li>
</ul>
<p>dbt already knows your graph. The package leans on that: objects are declared with <code>share_model</code> / <code>share_models</code> and <code>ref()</code>, so staging runs before the listing model, and semantic views (if you use them) stay in dependency order.</p>
<hr />
<h2>What it does (in one breath)</h2>
<ul>
<li><p>Custom materialization <code>organization_listing</code> for Internal Marketplace listings.</p>
</li>
<li><p><code>share_models([...])</code> (or <code>share_model</code>) to register what goes into the share; object types (table, view, semantic view, Cortex search service) are <strong>auto-detected</strong> at runtime so you are not hand-picking grant verbs for every object.</p>
</li>
<li><p><strong>Manifest as YAML</strong> under <code>config.meta.listing_manifest</code> in schema files — aligned with Snowflake’s <a href="https://docs.snowflake.com/en/user-guide/collaboration/listings/organizational/org-listing-manifest-reference">organization listing manifest reference</a>.</p>
</li>
<li><p><strong>Lifecycle</strong>: normal runs alter in place; <code>--full-refresh</code> is your escape hatch when you need drop/recreate semantics.</p>
</li>
<li><p>Optional <code>listing_ref()</code> macro for <a href="https://docs.snowflake.com/en/user-guide/collaboration/listings/organizational/org-listing-query">ULL-style</a> references on the producer side.</p>
</li>
</ul>
<p>There is also an <code>external_listing</code> materialization in the repo that I treat as a <strong>blueprint</strong> for public Marketplace flows — same ideas, different privileges and constraints. I’m focusing this post on <strong>organization listings</strong> because that’s where most internal sharing pain lives.</p>
<hr />
<h2>Install</h2>
<p>Add the package to <code>packages.yml</code> (pin a release tag you trust; the example below matches the sample project in the repo at time of writing):</p>
<pre><code class="language-yaml">packages:
  - git: "https://github.com/anthu/dbt-snowflake-listings.git"
    revision: v0.2.3
</code></pre>
<p>Then:</p>
<pre><code class="language-bash">dbt deps
</code></pre>
<p>You’ll need a Snowflake role that can create shares and organization listings (often something like <code>ACCOUNTADMIN</code> during a spike, or a dedicated role with the right grants). The package ships a <code>grant_listing_privileges</code> run-operation if you want to standardize that — see the repo’s <code>docs/macros.md</code>.</p>
<hr />
<h2>Minimal pattern: two files</h2>
<p>For a full example - please see the latest example in the repo itself. I will try to keep it in sync.</p>
<h3>1. Listing model (<code>.sql</code>)</h3>
<p>The model’s config selects the materialization and names the share. The body lists what gets granted into that share using <code>ref()</code> so dbt’s DAG stays honest:</p>
<pre><code class="language-sql">{{ config(
    materialized='organization_listing',
    meta={
        'share_name': 'TPCH_SAMPLE_SHARE',
        'publish': true,
    },
) }}

{{ dbt_snowflake_listings.share_models([
    ref('stg_tpch_nation'),
    ref('stg_tpch_region'),
    ref('stg_tpch_customer'),
    ref('stg_tpch_orders'),
]) }}
</code></pre>
<p>That snippet is lifted from the <strong>TPC-H sample</strong> example under <a href="https://github.com/anthu/dbt-snowflake-listings/tree/main/examples/snowflake_sample_data"><code>examples/snowflake_sample_data/</code></a> — it shares staging models built from <code>SNOWFLAKE_SAMPLE_DATA</code>, which is a nice zero-ingestion way to try the flow.</p>
<h3>2. Manifest (<code>.yml</code>)</h3>
<p>Keep prose and marketplace-facing fields in YAML next to the model. At minimum you want a clear <strong>title</strong>, <strong>description</strong>, and <strong>organization_targets</strong>; everything else maps to Snowflake’s manifest schema:</p>
<pre><code class="language-yaml">models:
  - name: tpch_sample_listing
    description: &gt;
      Organization listing that shares TPC-H benchmark sample tables with all
      accounts in the organization via the Internal Marketplace.
    config:
      meta:
        listing_manifest:
          title: "TPC-H Sample Data (tables)"
          description: |
            Sample data from the TPC-H benchmark dataset, sourced from
            Snowflake's SNOWFLAKE_SAMPLE_DATA database.
          organization_profile: "INTERNAL"
          organization_targets:
            access:
              - all_internal_accounts: true
          locations:
            access_regions:
              - name: "ALL"
          auto_fulfillment:
            refresh_type: "SUB_DATABASE"
            refresh_schedule: "10 MINUTE"
          usage_examples:
            - title: "Top customers by order volume"
              description: "Find the most active customers by number of orders placed"
              query: &gt;
                SELECT
                    c.CUSTOMER_NAME,
                    c.MARKET_SEGMENT,
                    COUNT(*) AS order_count,
                    SUM(o.TOTAL_PRICE) AS total_spend
                FROM STG_TPCH_CUSTOMER c
                JOIN STG_TPCH_ORDERS o ON c.CUSTOMER_KEY = o.CUSTOMER_KEY
                GROUP BY 1, 2
                ORDER BY order_count DESC
                LIMIT 20
</code></pre>
<p>Good <code>usage_examples</code> are worth the time: they show up in the listing experience and they force you to write SQL that actually matches what subscribers will query.</p>
<hr />
<h2>Run it</h2>
<pre><code class="language-bash">dbt run --select tpch_sample_listing+
</code></pre>
<p>Or run the whole project if your graph is small. On success you should see the share, grants, and listing aligned with what you declared — without maintaining a parallel script tree.</p>
<p>When you need a hard reset:</p>
<pre><code class="language-bash">dbt run --select tpch_sample_listing --full-refresh
</code></pre>
<hr />
<h2>Producer-side querying with <code>listing_ref</code></h2>
<p>If you want to reference shared objects via a <strong>Uniform Listing Locator</strong> from the producer project (where the models already live), the package exposes:</p>
<pre><code class="language-sql">SELECT *
FROM {{ dbt_snowflake_listings.listing_ref('MY_LISTING', ref('my_shared_table')) }}
</code></pre>
<p>Consumers in other accounts still see whatever names the listing exposes; this macro is mainly for keeping producer analytics consistent with the same DAG.</p>
<hr />
<h2>What I’d watch closely</h2>
<ul>
<li><p><strong>Privileges and org settings</strong> — listing creation fails in boring ways if the role is short a grant; bake that into your platform story early.</p>
</li>
<li><p><strong>Manifest vs reality</strong> — YAML typos or invalid combinations surface as Snowflake errors; treat manifest changes like DDL reviews.</p>
</li>
<li><p><strong>Experimental tier</strong> — I ship semver tags, but you should still pin and read the changelog when upgrading. If something breaks, open an issue on the repo; I’m motivated by real-world friction.</p>
</li>
</ul>
<hr />
<h2>Further reading</h2>
<ul>
<li><p><a href="https://docs.snowflake.com/en/user-guide/collaboration/listings/organizational/org-listing-manifest-reference">Organization listing manifest reference (Snowflake Docs)</a></p>
</li>
<li><p><a href="https://docs.snowflake.com/en/user-guide/collaboration/listings/organizational/org-listing-query">Querying organization listings with ULL</a></p>
</li>
<li><p><a href="https://github.com/anthu/dbt-snowflake-listings">dbt-snowflake-listings on GitHub</a> — README, <code>docs/configuration.md</code>, <code>docs/lifecycle.md</code>, <code>examples/snowflake_sample_data/</code></p>
</li>
</ul>
<hr />
<p>If you try it on a real internal listing, I’m curious whether the two-file pattern (SQL for the graph, YAML for the manifest) matches how your team already reviews dbt changes — or where it fights your process. That feedback is what turns an experiment into something durable.</p>
]]></content:encoded></item><item><title><![CDATA[From dbt Models to Snowflake Semantic Views: Best Practices for Cortex Analyst]]></title><description><![CDATA[Most teams I talk to already have a decent dbt project and are now looking at Semantic Views and Cortex Analyst. The question is usually:

“How do I reuse what we have in dbt instead of building a sec]]></description><link>https://anthu.dev/from-dbt-models-to-snowflake-semantic-views-best-practices-for-cortex-analyst</link><guid isPermaLink="true">https://anthu.dev/from-dbt-models-to-snowflake-semantic-views-best-practices-for-cortex-analyst</guid><dc:creator><![CDATA[Anton Huck]]></dc:creator><pubDate>Fri, 03 Apr 2026 01:19:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69664db126b81fb851c48b13/e6eccd7d-0ae8-4d61-b973-cef1179249f9.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most teams I talk to already have a decent dbt project and are now looking at <strong>Semantic Views</strong> and <strong>Cortex Analyst</strong>. The question is usually:</p>
<blockquote>
<p>“How do I reuse what we have in dbt instead of building a second semantic layer from scratch?”</p>
</blockquote>
<p>The good news: you don’t have to choose. dbt can stay your <strong>transformation + modeling</strong> workhorse, and Semantic Views can become the <strong>thin semantic layer</strong> that powers Cortex Analyst (and other consumers) on top.</p>
<p>In this post I’ll show my favorite pattern that work well in practice:</p>
<ul>
<li>Define Semantic Views <strong>inside</strong> dbt with a dedicated package.</li>
</ul>
<p>Along the way I’ll share a few design tips that made Cortex Analyst answers a lot more predictable.</p>
<hr />
<h2>Why add Semantic Views on top of dbt?</h2>
<p>dbt is great at turning raw data into clean <strong>dim/fact models</strong> and shared SQL logic.</p>
<p>Semantic Views pick up where dbt stops:</p>
<ul>
<li><p>They describe <strong>business concepts</strong> (dimensions, time dimensions, metrics, relationships).</p>
</li>
<li><p>They live as first‑class Snowflake objects (<code>SEMANTIC VIEW</code>) with proper governance and sharing.</p>
</li>
<li><p>They’re what <strong>Cortex Analyst</strong> uses to translate natural language into SQL.</p>
</li>
</ul>
<p>A simple way to think about it:</p>
<ul>
<li><p>dbt: <em>“What does the warehouse look like?”</em></p>
</li>
<li><p>Semantic Views: <em>“How does the business talk about this data?”</em></p>
</li>
</ul>
<hr />
<h2>Define Semantic Views in dbt (<code>Snowflake-Labs/dbt_semantic_view</code>)</h2>
<p>If you already treat dbt as your system of record, this is the most natural option.</p>
<ol>
<li>Add the package:</li>
</ol>
<pre><code class="language-yaml">packages:
  - package: Snowflake-Labs/dbt_semantic_view
    version: 1.0.3
</code></pre>
<ol>
<li>Create a model that uses the <code>semantic_view</code> materialization and points at your existing models:</li>
</ol>
<pre><code class="language-sql">{{ config(materialized = 'semantic_view', schema = 'SEMANTICS') }}

semantic_view:
  name: orders_analytics_sv

  tables:
    - name: dim_customers
      base_table: {{ ref('dim_customers') }}
      primary_key: customer_id

    - name: fct_orders
      base_table: {{ ref('fct_orders') }}
      primary_key: order_id
      time_dimensions:
        - name: order_date
          expr: order_date

  metrics:
    - name: total_revenue
      expr: "SUM(fct_orders.order_amount)"
</code></pre>
<ol>
<li>Run it like any other dbt model:</li>
</ol>
<pre><code class="language-bash">dbt run --select orders_analytics_sv
</code></pre>
<p>Behind the scenes this compiles to a <code>CREATE SEMANTIC VIEW</code> statement. You get:</p>
<ul>
<li><p>A managed Semantic View in Snowflake.</p>
</li>
<li><p>Version control and reviews via dbt.</p>
</li>
<li><p>The same deployment story as the rest of your project.</p>
</li>
</ul>
<p>This is ideal when you want <strong>one place</strong> (dbt) to define how tables, relationships, and metrics are wired.</p>
<hr />
<h2>Making Cortex Analyst happy</h2>
<p>Regardless of how you create Semantic Views, a few simple rules go a long way:</p>
<ul>
<li><p><strong>Keep them focused.</strong> Think in domains like “Orders &amp; Customers” or “Account Usage”, not “everything in the warehouse”.</p>
</li>
<li><p><strong>Model real business language.</strong> Use dimensions and metrics that match how people actually ask questions (“revenue”, “active customers”, “region”). Add synonyms if your org loves acronyms.</p>
</li>
<li><p><strong>Wire relationships explicitly.</strong> Many‑to‑one from facts to dimensions on clean keys avoids a lot of weird joins.</p>
</li>
<li><p><strong>Start with a handful of verified questions.</strong> For your first Semantic View, capture 5–10 real questions and the exact SQL you expect. Use those as guardrails when you iterate. (as of writing in Private Preview)</p>
</li>
</ul>
<p>With that in place, Cortex Analyst has a much easier time turning “show me revenue by customer segment for last quarter” into the SQL you would have written yourself.</p>
<hr />
<h2>A pragmatic migration path</h2>
<p>If I had to start from scratch on an existing dbt project, I’d do this:</p>
<ol>
<li><p>Pick one high‑value domain (e.g. product analytics, finance, account usage).</p>
</li>
<li><p>Create a <strong>single</strong> Semantic View for that domain using either option above.</p>
</li>
<li><p>Add 5–15 metrics that matter and a small set of verified questions.</p>
</li>
<li><p>Put it in front of real users, see which questions fail, and iterate.</p>
</li>
</ol>
<p>Once that loop feels smooth, repeat for the next domain.</p>
<p>You end up with a thin, governed semantic layer on top of dbt that unlocks natural language and other consumers—without throwing away the modeling work you already invested in.</p>
<hr />
<h2>Bonus: experimental dbt package for converting dbt semantic models to Semantic Views</h2>
<p>One thing I ran into while working on this: a lot of teams are already investing in <strong>dbt’s semantic layer</strong> (semantic models, measures, entities), but would still like to end up with <strong>Snowflake-native Semantic Views</strong> at the end of the day.</p>
<p>Instead of rewriting everything by hand, I started an <strong>experimental</strong> dbt package that tries to bridge exactly that gap:</p>
<blockquote>
<p><a href="https://github.com/anthu/dbt_semantic_view_converter"><code>anthu/dbt_semantic_view_converter</code></a> – very early, APIs may change, feedback highly welcome.</p>
</blockquote>
<p>The idea is simple:</p>
<ul>
<li><p>You define <strong>semantic models</strong> in dbt the way you normally would (in <code>schema.yml</code>).</p>
</li>
<li><p>You add a small <strong>semantic view model</strong> with a special materialization.</p>
</li>
<li><p>When you run <code>dbt run</code>, the package generates the corresponding <code>CREATE SEMANTIC VIEW</code> DDL for you and creates a Snowflake Semantic View based on that config.</p>
</li>
</ul>
<h3>How it works (high level)</h3>
<ol>
<li><strong>Install the package</strong> in <code>packages.yml</code>:</li>
</ol>
<pre><code class="language-yaml">packages:
  - git: "https://github.com/sfc-gh-ahuck/dbt_semantic_view_converter.git"
    revision: main
</code></pre>
<p>Then:</p>
<pre><code class="language-bash">dbt deps
dbt parse
</code></pre>
<ol>
<li><strong>Define your semantic model</strong> (dbt semantic layer) in <code>schema.yml</code>:</li>
</ol>
<pre><code class="language-yaml">semantic_models:
  - name: orders
    description: "Order fact table"
    model: ref('dim_orders')

    entities:
      - name: order_id
        type: primary
      - name: customer_id
        type: foreign

    dimensions:
      - name: order_date
        type: time
        type_params:
          time_granularity: day
      - name: order_status
        type: categorical

    measures:
      - name: order_total
        agg: sum
      - name: order_count
        expr: 1
        agg: sum
</code></pre>
<ol>
<li><strong>Create a corresponding “semantic view” model</strong> in dbt:</li>
</ol>
<pre><code class="language-sql">-- models/semantic_views/orders_semantic_view.sql

{{ config(
    materialized = 'semantic_view',
    schema = 'semantic_layer'
) }}

-- The SELECT itself is just a placeholder.
-- The package reads the semantic model config and generates the DDL.
SELECT 1 AS placeholder;
</code></pre>
<ol>
<li><strong>Run dbt</strong>:</li>
</ol>
<pre><code class="language-bash">dbt run --models orders_semantic_view
</code></pre>
<p>Behind the scenes, the package inspects your semantic model config and emits a <code>CREATE OR REPLACE SEMANTIC VIEW</code> statement with tables, relationships, dimensions, facts, and metrics wired up according to that definition.</p>
<p>The end result looks roughly like:</p>
<pre><code class="language-sql">CREATE OR REPLACE SEMANTIC VIEW analytics.semantic_layer.orders
  COMMENT = 'Order fact table'
  /* TABLES, RELATIONSHIPS, DIMENSIONS, METRICS ... */
  COPY GRANTS;
</code></pre>
<h3>Why this might be interesting</h3>
<p>If you’re already invested in dbt’s semantic layer, this gives you a path to:</p>
<ul>
<li><p>Keep <strong>one definition of business logic</strong> (semantic models in dbt),</p>
</li>
<li><p>But still end up with <strong>Snowflake-native Semantic Views</strong> that Cortex Analyst and other tools can consume directly,</p>
</li>
<li><p>While reusing dbt’s existing workflows for <strong>dependencies, tests, docs, and CI/CD</strong>.</p>
</li>
</ul>
<p>It’s especially handy for teams who:</p>
<ul>
<li><p>Don’t want to maintain two separate semantic definitions,</p>
</li>
<li><p>Prefer reviewing semantic changes via PRs in the dbt repo,</p>
</li>
<li><p>And like the idea of “dbt is the source of truth, Snowflake Semantic Views are the runtime interface”.</p>
</li>
</ul>
<h3>Please treat it as experimental</h3>
<p>This is very much a <strong>work-in-progress</strong>:</p>
<ul>
<li><p>The materialization name, config options, and generated SQL shape may still change.</p>
</li>
<li><p>Error messages and guardrails are basic.</p>
</li>
<li><p>I’m still figuring out what the “right” abstraction level is (how much of the Snowflake DDL to expose vs hide).</p>
</li>
</ul>
<p>So: <strong>don’t</strong> drop it straight into mission‑critical projects yet.</p>
<p>If you do try it out in a sandbox or side‑project, I’d really love feedback:</p>
<ul>
<li><p>Does the workflow fit how you use dbt today?</p>
</li>
<li><p>Is the mapping from semantic models → Semantic View what you expect?</p>
</li>
<li><p>What would you need before trusting this in a real environment?</p>
</li>
</ul>
<p>Issues, discussions, and PRs are all welcome in the repo:</p>
<p>👉 <a href="https://github.com/anthu/dbt_semantic_view_converter"><code>anthu/dbt_semantic_view_converter</code> on GitHub</a></p>
<hr />
<h2>Further reading</h2>
<ul>
<li><p><a href="https://docs.snowflake.com/en/user-guide/views-semantic/best-practices-dev">Best practices for semantic views (Snowflake Docs)</a></p>
</li>
<li><p><a href="https://docs.snowflake.com/en/user-guide/ui-snowsight-data-databases-view">Using Snowsight to create and manage semantic views</a>e semantic views</p>
</li>
<li><p><a href="https://github.com/Snowflake-Labs/dbt_semantic_view"><code>dbt_semantic_view</code> package on dbt Hub</a></p>
</li>
<li><p><a href="https://github.com/anthu/dbt_semantic_view_converter">GitHub - anthu/dbt_semantic_view_converter · GitHub</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Multi-Model Cost Optimization with Snowflake Cortex and OpenClaw]]></title><description><![CDATA[In Part 1 I connected OpenClaw to Snowflake Cortex as the LLM backend. Enterprise-grade security, unified billing, data stays in Snowflake. So far so good.
But after running it for a while I noticed something: most of the tokens OpenClaw burns throug...]]></description><link>https://anthu.dev/multi-model-cost-optimization-with-snowflake-cortex-and-openclaw</link><guid isPermaLink="true">https://anthu.dev/multi-model-cost-optimization-with-snowflake-cortex-and-openclaw</guid><category><![CDATA[snowflake]]></category><category><![CDATA[openclaw]]></category><category><![CDATA[AI]]></category><category><![CDATA[agents]]></category><category><![CDATA[agentic AI]]></category><dc:creator><![CDATA[Anton Huck]]></dc:creator><pubDate>Tue, 17 Feb 2026 18:36:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/g494Z7pba14/upload/47d02e6d2b55bb2f9fabd9ffc16ca2b3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In <a target="_blank" href="https://anthu.dev/connecting-openclaw-to-snowflake-cortex">Part 1</a> I connected OpenClaw to Snowflake Cortex as the LLM backend. Enterprise-grade security, unified billing, data stays in Snowflake. So far so good.</p>
<p>But after running it for a while I noticed something: most of the tokens OpenClaw burns through aren't from the main agent doing complex reasoning. They're from subagents doing file searches, reading docs, and grepping through code. Simple stuff. And all of that was running on the same expensive model as the main agent.</p>
<p>My naive approach was using <code>claude-sonnet-4-5</code> for everything. And you guessed it - the bill reflected that.</p>
<h2 id="heading-the-key-insight-not-every-task-needs-a-3-model">The Key Insight: Not Every Task Needs a $3 Model</h2>
<p>OpenClaw uses a hierarchical architecture. The main agent does the thinking - planning, reasoning, decision-making. But it delegates a lot of the grunt work to subagents: searching files, reading documentation, generating boilerplate code. These tasks are straightforward enough that a $0.03 model handles them just fine.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771353697770/9458d42f-bbbf-45e7-8f1f-838eaade3ee5.png" alt class="image--center mx-auto" /></p>
<p>The main agent stays on the best model. The subagents run on whatever is cheapest for their specific job.</p>
<h2 id="heading-what-cheap-models-can-handle">What Cheap Models Can Handle</h2>
<p>I spent some time testing which models are "good enough" for different subagent tasks. Here's what I found:</p>
<p><strong>Ultra-cheap ($0.03-$0.06)</strong> works perfectly for file search, grep, listing directories, and reading/summarizing docs. These are essentially pattern matching tasks. <code>llama3.1-8b</code> at $0.03/$0.03 per 1M tokens is my go-to here. For doc summarization, <code>openai-gpt-5-nano</code> at $0.06/$0.44 does a solid job.</p>
<p><strong>Budget models ($0.12-$0.25)</strong> are good for boilerplate code generation, test scaffolding, simple refactoring, and config file generation. <code>snowflake-llama-3.3-70b</code> at $0.12/$0.12 is particularly interesting here because Snowflake tuned it specifically for their workloads. <code>llama3.1-70b</code> at $0.25/$0.25 handles general code generation well.</p>
<p><strong>Mid-tier ($1.00-$1.25)</strong> is where you go when you actually need reasoning: code reviews, bug analysis, API integrations. <code>claude-haiku-4-5</code> at $1.00/$5.00 or <code>openai-gpt-5</code> at $1.25/$10.00.</p>
<h2 id="heading-the-numbers-dont-lie">The Numbers Don't Lie</h2>
<p>Let's do the math on a typical exploration-heavy session. Say 100K input tokens and 50K output tokens total, with about 80% of that going to subagents (which is realistic for codebase exploration).</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Configuration</td><td>Main Agent Cost</td><td>Subagent Cost</td><td><strong>Total</strong></td></tr>
</thead>
<tbody>
<tr>
<td>All Sonnet</td><td>$0.30</td><td>$0.75</td><td>$1.05</td></tr>
<tr>
<td>Sonnet + Haiku</td><td>$0.06</td><td>$0.28</td><td>$0.34</td></tr>
<tr>
<td>Sonnet + llama3.1-8b</td><td>$0.06</td><td><strong>$0.004</strong></td><td><strong>$0.064</strong></td></tr>
<tr>
<td>Sonnet + llama3.1-70b</td><td>$0.06</td><td><strong>$0.03</strong></td><td><strong>$0.09</strong></td></tr>
</tbody>
</table>
</div><p>That's a <strong>94% cost reduction</strong> going from all-Sonnet to Sonnet + llama3.1-8b subagents. Even compared to Haiku subagents, llama3.1-8b is 97% cheaper on input and 99% cheaper on output.</p>
<p>What a difference.</p>
<h2 id="heading-configuration">Configuration</h2>
<p>The setup lives in two places: <code>~/.openclaw/openclaw.json</code> for the provider config and <code>~/.openclaw/agents/main/agent/models.json</code> for model definitions. Here's my exploration-heavy config that I use most of the time:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"providers"</span>: {
    <span class="hljs-attr">"cortex"</span>: {
      <span class="hljs-attr">"baseUrl"</span>: <span class="hljs-string">"https://&lt;org&gt;-&lt;account&gt;.snowflakecomputing.com/api/v2/cortex/v1"</span>,
      <span class="hljs-attr">"apiKey"</span>: <span class="hljs-string">"&lt;your-pat-token&gt;"</span>,
      <span class="hljs-attr">"api"</span>: <span class="hljs-string">"openai-completions"</span>,
      <span class="hljs-attr">"models"</span>: [
        {
          <span class="hljs-attr">"id"</span>: <span class="hljs-string">"claude-sonnet-4-5"</span>,
          <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Cortex Claude Sonnet 4.5"</span>,
          <span class="hljs-attr">"reasoning"</span>: <span class="hljs-literal">true</span>,
          <span class="hljs-attr">"input"</span>: [<span class="hljs-string">"text"</span>, <span class="hljs-string">"image"</span>],
          <span class="hljs-attr">"contextWindow"</span>: <span class="hljs-number">200000</span>,
          <span class="hljs-attr">"maxTokens"</span>: <span class="hljs-number">16384</span>,
          <span class="hljs-attr">"cost"</span>: {<span class="hljs-attr">"input"</span>: <span class="hljs-number">3.00</span>, <span class="hljs-attr">"output"</span>: <span class="hljs-number">15.00</span>, <span class="hljs-attr">"cacheRead"</span>: <span class="hljs-number">0.30</span>, <span class="hljs-attr">"cacheWrite"</span>: <span class="hljs-number">3.75</span>},
          <span class="hljs-attr">"compat"</span>: {<span class="hljs-attr">"supportsDeveloperRole"</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">"maxTokensField"</span>: <span class="hljs-string">"max_completion_tokens"</span>}
        },
        {
          <span class="hljs-attr">"id"</span>: <span class="hljs-string">"llama3.1-8b"</span>,
          <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Cortex Llama 3.1 8B"</span>,
          <span class="hljs-attr">"reasoning"</span>: <span class="hljs-literal">false</span>,
          <span class="hljs-attr">"input"</span>: [<span class="hljs-string">"text"</span>],
          <span class="hljs-attr">"contextWindow"</span>: <span class="hljs-number">32000</span>,
          <span class="hljs-attr">"maxTokens"</span>: <span class="hljs-number">8192</span>,
          <span class="hljs-attr">"cost"</span>: {<span class="hljs-attr">"input"</span>: <span class="hljs-number">0.03</span>, <span class="hljs-attr">"output"</span>: <span class="hljs-number">0.03</span>, <span class="hljs-attr">"cacheRead"</span>: <span class="hljs-number">0</span>, <span class="hljs-attr">"cacheWrite"</span>: <span class="hljs-number">0</span>},
          <span class="hljs-attr">"compat"</span>: {<span class="hljs-attr">"supportsDeveloperRole"</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">"maxTokensField"</span>: <span class="hljs-string">"max_completion_tokens"</span>}
        }
      ]
    }
  },
  <span class="hljs-attr">"agents"</span>: {
    <span class="hljs-attr">"defaults"</span>: {
      <span class="hljs-attr">"model"</span>: {<span class="hljs-attr">"primary"</span>: <span class="hljs-string">"cortex/claude-sonnet-4-5"</span>},
      <span class="hljs-attr">"subagents"</span>: {
        <span class="hljs-attr">"maxConcurrent"</span>: <span class="hljs-number">8</span>,
        <span class="hljs-attr">"model"</span>: <span class="hljs-string">"cortex/llama3.1-8b"</span>
      }
    }
  }
}
</code></pre>
<p>The important bit is the <code>cost</code> field on each model. With those configured, the Openclaw dashboard actually tracks your spend per model - so you can see exactly where the money goes.</p>
<h2 id="heading-workflow-specific-setups">Workflow-Specific Setups</h2>
<p>I switch between a few configurations depending on what I'm doing:</p>
<p><strong>Exploration &amp; file search</strong>: Sonnet main + <code>llama3.1-8b</code> subagents ($0.03/$0.03). This is my default. 97% cheaper than Haiku subagents and perfectly fine for grepping, finding files, and navigating codebases.</p>
<p><strong>Documentation &amp; research</strong>: Sonnet main + <code>openai-gpt-5-nano</code> subagents ($0.06/$0.44). Slightly more capable for summarization tasks but still 94% cheaper on input than Haiku.</p>
<p><strong>Code generation</strong>: Sonnet main + <code>llama3.1-70b</code> subagents ($0.25/$0.25). When the subagents need to write actual code rather than just find files. Balanced quality/cost.</p>
<p><strong>Snowflake-specific work</strong>: Sonnet main + <code>snowflake-llama-3.3-70b</code> subagents ($0.12/$0.12). Snowflake's own tuned model. 88% cheaper than Haiku and optimized for SQL generation and data tasks. I use this when working on Snowflake projects specifically.</p>
<p><strong>Maximum quality</strong>: When I'm working on critical production code or complex architecture and cost doesn't matter - <code>claude-opus-4-6</code> main + <code>claude-sonnet-4-5</code> subagents. Premium everything. But honestly I rarely need this.</p>
<h2 id="heading-monitoring-what-you-spend">Monitoring What You Spend</h2>
<p>Beyond the Openclaw dashboard, you can query actual consumption directly from Snowflake:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> 
    MODEL_NAME,
    <span class="hljs-keyword">SUM</span>(INPUT_TOKENS) <span class="hljs-keyword">as</span> total_input_tokens,
    <span class="hljs-keyword">SUM</span>(OUTPUT_TOKENS) <span class="hljs-keyword">as</span> total_output_tokens,
    <span class="hljs-keyword">SUM</span>(CREDITS_USED) <span class="hljs-keyword">as</span> total_credits
<span class="hljs-keyword">FROM</span> SNOWFLAKE.ACCOUNT_USAGE.CORTEX_REST_API_USAGE_HISTORY
<span class="hljs-keyword">WHERE</span> START_TIME &gt;= <span class="hljs-keyword">DATEADD</span>(<span class="hljs-keyword">day</span>, <span class="hljs-number">-7</span>, <span class="hljs-keyword">CURRENT_TIMESTAMP</span>())
<span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span> MODEL_NAME
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> total_credits <span class="hljs-keyword">DESC</span>;
</code></pre>
<p>This gives you the ground truth on what's actually being consumed. I run this weekly to make sure my assumptions about subagent token distribution still hold.</p>
<p>But you don't always want to write SQL just to check how things are going. Openclaw itself ships with a usage view that breaks down token consumption and cost per model, per session. Once you have the <code>cost</code> fields configured in your model definitions (as shown above), the dashboard picks them up automatically and gives you a nice overview of where your tokens are going. In my case the split is pretty obvious - the main agent shows up as one big chunk and the subagent calls are spread across dozens of small, cheap requests.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771354018314/8d5a2560-9dea-4c35-80e0-9fa70e0a3362.png" alt class="image--center mx-auto" /></p>
<p>What I like about this view is that you can immediately see if a subagent task is burning more tokens than expected. If one of the llama3.1-8b calls suddenly shows high token counts, that's usually a sign that the task is too complex for the cheap model and should be bumped up a tier. Most of the time though, the numbers confirm what you'd expect: the majority of subagent calls are tiny and cheap.</p>
<h2 id="heading-available-models-at-a-glance">Available Models at a Glance</h2>
<p>Cortex currently offers 22 models across the REST API. Here are the ones I find most relevant for Openclaw setups, grouped by what I'd actually use them for:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Tier</td><td>Model</td><td>Input/Output ($/1M tokens)</td><td>Use Case</td></tr>
</thead>
<tbody>
<tr>
<td>Premium</td><td>claude-opus-4-6</td><td>$5.00/$25.00</td><td>Main agent (max quality)</td></tr>
<tr>
<td>Standard</td><td>claude-sonnet-4-5</td><td>$3.00/$15.00</td><td>Main agent (recommended)</td></tr>
<tr>
<td>Budget</td><td>claude-haiku-4-5</td><td>$1.00/$5.00</td><td>Quality subagents</td></tr>
<tr>
<td>Budget</td><td>llama3.1-70b</td><td>$0.25/$0.25</td><td>Code generation subagents</td></tr>
<tr>
<td>Ultra-Budget</td><td>snowflake-llama-3.3-70b</td><td>$0.12/$0.12</td><td>Snowflake-specific subagents</td></tr>
<tr>
<td>Ultra-Budget</td><td>openai-gpt-5-nano</td><td>$0.06/$0.44</td><td>Doc summarization subagents</td></tr>
<tr>
<td>Ultra-Budget</td><td>llama3.1-8b</td><td>$0.03/$0.03</td><td>File search subagents</td></tr>
<tr>
<td>Ultra-Budget</td><td>mistral-7b</td><td>$0.03/$0.03</td><td>Pattern matching subagents</td></tr>
</tbody>
</table>
</div><p>There are more models available (deepseek-r1, llama4-maverick, mistral-large2, openai-o4-mini, etc.) but these are the ones I actually use regularly.</p>
<h2 id="heading-practical-tips">Practical Tips</h2>
<p><strong>Start ultra-cheap.</strong> Use <code>llama3.1-8b</code> for all subagents first. Upgrade individual task types only when you notice quality issues. You'd be surprised how rarely that happens for file search and navigation tasks.</p>
<p><strong>Match model to task, not to habit.</strong> It's tempting to use Haiku everywhere because it's "the cheap Claude model." But for most subagent tasks, it's leaving money on the table. A $0.03 model that searches files is just as good as a $1.00 model for that specific job.</p>
<p><strong>Use prompt caching.</strong> Cortex supports prompt caching for OpenAI and Anthropic models. For OpenAI models it's implicit (kicks in at 1024+ tokens). For Anthropic models you need to add cache points in the request. Either way, it cuts repeated context costs dramatically.</p>
<p><strong>Run subagents in parallel.</strong> With <code>maxConcurrent: 8</code> and $0.03 subagents, you can do a lot of exploration in parallel for almost nothing. Much better than sequentially running one expensive agent.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>The main takeaway: most of the tokens your AI coding agent burns through are on simple tasks. File search, grep, doc reading - these don't need expensive models. By routing them to $0.03 models via Snowflake Cortex, you keep the quality where it matters (the main agent) while cutting overall costs by 90%+.</p>
<p>And because everything runs through Cortex, you get unified billing, enterprise security, and the ability to monitor actual usage through Snowflake's account usage views. No separate API keys to manage, no surprise bills from different providers.</p>
]]></content:encoded></item><item><title><![CDATA[Connecting OpenClaw to Snowflake Cortex]]></title><description><![CDATA[I've been playing around with OpenClaw - an open-source AI assistant framework - and wanted to hook it up to Snowflake's Cortex LLM API. The idea: use enterprise-grade models like Claude Sonnet 4.5 through Snowflake's infrastructure while keeping my ...]]></description><link>https://anthu.dev/connecting-openclaw-to-snowflake-cortex</link><guid isPermaLink="true">https://anthu.dev/connecting-openclaw-to-snowflake-cortex</guid><category><![CDATA[snowflake cortex]]></category><category><![CDATA[snowflake]]></category><category><![CDATA[snowflake tutorial]]></category><category><![CDATA[openclaw]]></category><dc:creator><![CDATA[Anton Huck]]></dc:creator><pubDate>Tue, 17 Feb 2026 07:05:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/TcN2ucbpBQg/upload/2ab07cb08ed3cd538e4bbc730227294a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've been playing around with <a target="_blank" href="https://openclaw.ai">OpenClaw</a> - an open-source AI assistant framework - and wanted to hook it up to Snowflake's Cortex LLM API. The idea: use enterprise-grade models like Claude Sonnet 4.5 through Snowflake's infrastructure while keeping my existing config as a fallback. Sounds straightforward, right?</p>
<p>Well, it kind of is. And kind of isn't. The integration itself is surprisingly clean, but getting there involved a few detours I didn't expect.</p>
<h2 id="heading-why-cortex">Why Cortex?</h2>
<p>Here's the thing most people don't realize about Snowflake Cortex: it exposes a Chat Completions API that's a superset of the OpenAI API. That means any tool that supports OpenAI can - in theory - connect to Snowflake with minimal changes. You get Claude, GPT, Llama, Mistral and others through a single endpoint, all billed through Snowflake credits. Plus the usual enterprise goodies: network policies, PAT tokens with role restrictions, audit logging. All built-in.</p>
<p>So far so good.</p>
<h2 id="heading-setting-up-a-least-privilege-service-account">Setting Up a Least-Privilege Service Account</h2>
<p>Rather than reusing an admin account (please don't), I created a dedicated service user. The <code>SNOWFLAKE.CORTEX_USER</code>database role grants access to Cortex LLM functions - nothing more. No data access, no warehouse modification rights.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create a role with only Cortex access</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">ROLE</span> JEEVES_ROLE;

<span class="hljs-comment">-- Grant the Cortex User database role (required for REST API)</span>
<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">DATABASE</span> <span class="hljs-keyword">ROLE</span> SNOWFLAKE.CORTEX_USER <span class="hljs-keyword">TO</span> <span class="hljs-keyword">ROLE</span> JEEVES_ROLE;

<span class="hljs-comment">-- Grant warehouse usage (no OPERATE/MODIFY - just usage)</span>
<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">USAGE</span> <span class="hljs-keyword">ON</span> WAREHOUSE TASK_WH <span class="hljs-keyword">TO</span> <span class="hljs-keyword">ROLE</span> JEEVES_ROLE;
</code></pre>
<p>Since the service runs from a known IP range, I also locked it down with a network rule and policy:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create a network rule for the service's IP range</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> NETWORK RULE ADMIN_DB.NETWORK_POLICY_MGMT.JEEVES_SERVICE
  <span class="hljs-keyword">MODE</span> = INGRESS
  <span class="hljs-keyword">TYPE</span> = IPV4
  VALUE_LIST = (<span class="hljs-string">'10.0.1.0/24'</span>);  <span class="hljs-comment">-- Could be a VPC CIDR, a static IP, whatever fits your setup</span>

<span class="hljs-comment">-- Create a network policy referencing the rule</span>
<span class="hljs-keyword">CREATE</span> NETWORK <span class="hljs-keyword">POLICY</span> JEEVES_POLICY
  ALLOWED_NETWORK_RULE_LIST = (ADMIN_DB.NETWORK_POLICY_MGMT.JEEVES_SERVICE);
</code></pre>
<p>Even if the PAT token gets compromised, it can only be used from the allowed IP range. Belt and suspenders.</p>
<p>For the user itself, Snowflake's <code>TYPE = SERVICE</code> is the right choice for programmatic access:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">USER</span> JEEVES
  <span class="hljs-keyword">TYPE</span> = SERVICE
  DEFAULT_ROLE = JEEVES_ROLE
  DEFAULT_WAREHOUSE = TASK_WH
  NETWORK_POLICY = JEEVES_POLICY
  <span class="hljs-keyword">COMMENT</span> = <span class="hljs-string">'AI assistant service account'</span>;

<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">ROLE</span> JEEVES_ROLE <span class="hljs-keyword">TO</span> <span class="hljs-keyword">USER</span> JEEVES;
</code></pre>
<p>And for auth, a PAT token with role restriction:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">USER</span> JEEVES <span class="hljs-keyword">ADD</span> PROGRAMMATIC <span class="hljs-keyword">ACCESS</span> TOKEN 
  JEEVES_PAT
  ROLE_RESTRICTION = <span class="hljs-string">'JEEVES_ROLE'</span>
  DAYS_TO_EXPIRY = <span class="hljs-number">365</span>;
</code></pre>
<p>The <code>ROLE_RESTRICTION</code> bit is important - it prevents the token from being used with elevated privileges even if someone grants additional roles to the user later. And heads up: the token secret is only shown once at creation time. Save it immediately.</p>
<h2 id="heading-configuring-the-application">Configuring the Application</h2>
<p>The Cortex Chat Completions API endpoint follows this pattern:</p>
<pre><code class="lang-bash">https://&lt;org&gt;-&lt;account_name&gt;.snowflakecomputing.com/api/v2/cortex/v1
</code></pre>
<p>One thing that tripped me up right away: you need to use your <strong>account name</strong>, not the account locator. You can check yours with:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> CURRENT_ORGANIZATION_NAME() || <span class="hljs-string">'-'</span> || CURRENT_ACCOUNT_NAME();
<span class="hljs-comment">-- e.g. returns: myorganization-myaccount</span>
</code></pre>
<p>So the URL becomes: <code>https://myorganization-myaccount.snowflakecomputing.com/api/v2/cortex/v1</code></p>
<p>For OpenClaw, I added a new provider using the OpenAI-compatible API:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"env"</span>: {
    <span class="hljs-attr">"CORTEX_API_KEY"</span>: <span class="hljs-string">"&lt;JEEVES_PAT_TOKEN&gt;"</span>
  },
  <span class="hljs-attr">"models"</span>: {
    <span class="hljs-attr">"mode"</span>: <span class="hljs-string">"merge"</span>,
    <span class="hljs-attr">"providers"</span>: {
      <span class="hljs-attr">"cortex"</span>: {
        <span class="hljs-attr">"baseUrl"</span>: <span class="hljs-string">"https://&lt;orgname&gt;-&lt;account_name&gt;.snowflakecomputing.com/api/v2/cortex/v1"</span>,
        <span class="hljs-attr">"apiKey"</span>: <span class="hljs-string">"${CORTEX_API_KEY}"</span>,
        <span class="hljs-attr">"api"</span>: <span class="hljs-string">"openai-completions"</span>,
        <span class="hljs-attr">"models"</span>: [
          {
            <span class="hljs-attr">"id"</span>: <span class="hljs-string">"claude-sonnet-4-5"</span>,
            <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Cortex Sonnet 4.5"</span>,
            <span class="hljs-attr">"reasoning"</span>: <span class="hljs-literal">true</span>,
            <span class="hljs-attr">"input"</span>: [<span class="hljs-string">"text"</span>, <span class="hljs-string">"image"</span>],
            <span class="hljs-attr">"contextWindow"</span>: <span class="hljs-number">200000</span>,
            <span class="hljs-attr">"maxTokens"</span>: <span class="hljs-number">16384</span>,
            <span class="hljs-attr">"compat"</span>: {
              <span class="hljs-attr">"maxTokensField"</span>: <span class="hljs-string">"max_completion_tokens"</span>,
              <span class="hljs-attr">"supportsDeveloperRole"</span>: <span class="hljs-literal">false</span>
            }
          }
        ]
      }
    }
  },
  <span class="hljs-attr">"agents"</span>: {
    <span class="hljs-attr">"defaults"</span>: {
      <span class="hljs-attr">"model"</span>: {
        <span class="hljs-attr">"primary"</span>: <span class="hljs-string">"cortex/claude-sonnet-4-5"</span>,
        <span class="hljs-attr">"fallbacks"</span>: [<span class="hljs-string">"snowflake/claude-sonnet-4-5"</span>]
      }
    }
  }
}
</code></pre>
<p>See that <code>compat</code> object? Those two settings are doing the heavy lifting. More on that in a second.</p>
<h2 id="heading-the-troubleshooting-journey">The Troubleshooting Journey</h2>
<p>Getting this working wasn't exactly a smooth ride. Here's what actually happened.</p>
<h3 id="heading-404-not-found">404 Not Found</h3>
<p>My first attempt used the account locator in the URL. 404. The fix was trivially simple once I figured it out - use the account name, not the locator:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> CURRENT_ACCOUNT_NAME();  <span class="hljs-comment">-- Returns your account name (use this)</span>
<span class="hljs-keyword">SELECT</span> CURRENT_ACCOUNT();       <span class="hljs-comment">-- Returns the account locator (don't use this!)</span>
</code></pre>
<h3 id="heading-developer-messages-are-not-supported">"developer messages are not supported"</h3>
<p>The proxy revealed it. OpenAI introduced a <code>developer</code> message role for reasoning models like o1. When Openclaw detects a reasoning model (<code>"reasoning": true</code>), it sends system prompts with <code>role: "developer"</code> instead of <code>role: "system"</code>. Cortex doesn't support this.</p>
<p>Fix: <code>"supportsDeveloperRole": false</code> in the model's <code>compat</code> settings.</p>
<h3 id="heading-maxtokens-is-deprecated">"max_tokens is deprecated"</h3>
<p>Earlier testing with curl already revealed this one. OpenAI's newer API uses <code>max_completion_tokens</code> instead of the legacy <code>max_tokens</code>. Cortex follows this convention strictly and will reject requests using the old parameter.</p>
<p>Fix: <code>"maxTokensField": "max_completion_tokens"</code> in the model's <code>compat</code> settings.</p>
<h3 id="heading-and-it-works">And It Works</h3>
<p>After applying both fixes:</p>
<pre><code class="lang-bash">$ openclaw agent --<span class="hljs-built_in">local</span> -m <span class="hljs-string">"Say hello in one word"</span> --session-id <span class="hljs-built_in">test</span>
Hello!
</code></pre>
<p>What a relief.</p>
<h2 id="heading-what-i-learned">What I Learned</h2>
<p>There are a few debugging lessons worth highlighting. First, use a proxy when debugging API integrations - it reveals the actual request and response bodies, especially when responses are compressed. Second, "OpenAI-compatible" doesn't mean identical. Providers implement different subsets of the API, and the edge cases will get you. And third, 400 errors often have descriptive JSON bodies if you can get past the gzip encoding.</p>
<h2 id="heading-security-layers">Security Layers</h2>
<p>For those keeping score, here's what the setup looks like from a security perspective. Authentication goes through a PAT token (not a password). Authorization is role-restricted to <code>CORTEX_USER</code> only. Network access is limited to an IP allowlist via network policy. The service account has zero data access - Cortex API only. The PAT is role-restricted so it can't be used to escalate privileges. The warehouse grants <code>USAGE</code> only with no modify rights. And the token expires after 365 days with rotation capability.</p>
<h2 id="heading-the-result">The Result</h2>
<p>OpenClaw now uses Snowflake Cortex as its primary LLM provider, with an existing Snowflake Anthropic endpoint as a fallback. All AI inference routes through my Snowflake account, which gives me centralized billing, enterprise audit logging, network-layer security, consistent access to the latest models, and automatic failover if the primary is unavailable.</p>
<p>The key takeaways: use <code>TYPE = SERVICE</code> for programmatic users. Always use <code>ROLE_RESTRICTION</code> on PAT tokens. Network policies add a real extra layer for service accounts. The OpenAI-compatible endpoint at <code>/api/v2/cortex/v1</code> makes integration with existing tools surprisingly straightforward - once you work through the quirks.</p>
<p>If you're running any tool that speaks OpenAI, connecting it to Cortex is worth the effort.</p>
<h2 id="heading-whats-next">What's Next</h2>
<p>Now that the plumbing is in place, the interesting part begins. Cortex gives you access to a whole range of models through a single endpoint - <strong>Claude, GPT, Llama, Mistral, and more</strong>. That means you can start matching the right model to the right task instead of throwing your most expensive model at everything.</p>
<p><strong>My plan is to set up specialized agents</strong>: a lightweight model like Haiku for quick summarization and triage, something like Llama for code generation where you need fast iteration, and Sonnet for the heavy lifting - complex reasoning, architecture decisions, that kind of thing. Same endpoint, same auth, same billing. You just swap the model ID in the config and you're done.</p>
<p>The cost savings add up quickly. Not every task needs the biggest model, and with Cortex you don't need separate API keys, billing accounts, or provider integrations to find that out. One service account, one network policy, multiple models - each doing what it's best at.</p>
<p>If you're running any tool that speaks OpenAI, connecting it to Cortex is worth the effort.</p>
<p><strong>The write-up on this follows in the next days! - Subscribe today</strong></p>
<hr />
<p><strong>Resources:</strong></p>
<ul>
<li><p><a target="_blank" href="https://docs.snowflake.com/en/user-guide/snowflake-cortex/open_ai_sdk">Cortex Chat Completions API Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://docs.snowflake.com/en/user-guide/programmatic-access-tokens">Programmatic Access Tokens</a></p>
</li>
<li><p><a target="_blank" href="https://docs.snowflake.com/en/user-guide/network-policies">Network Policies</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[The Hidden Trap in Snowflake's INFER_SCHEMA]]></title><description><![CDATA[I've always been a fan of Snowflake's INFER_SCHEMA() function. Not only because it saves you from manually typing out column definitions but the whole idea of letting Snowflake figure out your schema from actual data feels like the right level of aut...]]></description><link>https://anthu.dev/the-hidden-trap-in-snowflakes-inferschema</link><guid isPermaLink="true">https://anthu.dev/the-hidden-trap-in-snowflakes-inferschema</guid><category><![CDATA[snowflake-bites]]></category><category><![CDATA[snowflake]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[Snowpipe]]></category><dc:creator><![CDATA[Anton Huck]]></dc:creator><pubDate>Tue, 13 Jan 2026 20:51:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/H55X9zjmM1A/upload/0aa99389916aad159fbfb40514a5688d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've always been a fan of Snowflake's <code>INFER_SCHEMA()</code> function. Not only because it saves you from manually typing out column definitions but the whole idea of letting Snowflake figure out your schema from actual data feels like the right level of automation. But recently I had to learn its limitations the hard way.</p>
<h2 id="heading-the-problem-that-bit-me">The Problem That Bit Me</h2>
<p>Here's what happened: I loaded a sample file with IDs like <code>1</code>, <code>2</code>, <code>3</code> — single-digit integers. <code>INFER_SCHEMA()</code> happily inferred <code>NUMBER(1,0)</code> for the column. Makes sense, right? Minimal precision for single digits.</p>
<p>Then production data arrived with IDs in the millions.</p>
<pre><code class="lang-json">Numeric value '<span class="hljs-number">1234567</span>' is out of range
</code></pre>
<p>What a bummer.</p>
<p>The issue is that <code>INFER_SCHEMA()</code> acts <em>too</em> precise. It looks at your sample data and picks the tightest type that fits. A file with values like <code>1.5</code> and <code>2.3</code> gets inferred as <code>NUMBER(2,1)</code> — which explodes when <code>123.456</code> shows up later.</p>
<h2 id="heading-the-template-pattern-nobody-talks-about">The Template Pattern Nobody Talks About</h2>
<p>Here's the thing: when you create a table using <code>USING TEMPLATE</code>, you're not locked into the raw output of <code>INFER_SCHEMA()</code>. The template is just SQL — and you can transform it however you want.</p>
<p>Most tutorials show you this basic pattern:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> my_table
<span class="hljs-keyword">USING</span> <span class="hljs-keyword">TEMPLATE</span> (
    <span class="hljs-keyword">SELECT</span> ARRAY_AGG(OBJECT_CONSTRUCT(*))
    <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">TABLE</span>(INFER_SCHEMA(...))
);
</code></pre>
<p>So far so good. But that <code>OBJECT_CONSTRUCT(*)</code> is where the magic happens — or doesn't happen if you're just passing everything through unchanged.</p>
<h2 id="heading-sql-functions-to-the-rescue">SQL Functions to the Rescue</h2>
<p>You can use any SQL function inside the template to transform column names, types, or other properties. Let me show you what I mean.</p>
<p><strong>Uppercasing column names:</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> my_table
<span class="hljs-keyword">USING</span> <span class="hljs-keyword">TEMPLATE</span> (
    <span class="hljs-keyword">SELECT</span> ARRAY_AGG(
        OBJECT_CONSTRUCT(
            <span class="hljs-string">'COLUMN_NAME'</span>, <span class="hljs-keyword">UPPER</span>(COLUMN_NAME),  <span class="hljs-comment">-- Force uppercase</span>
            <span class="hljs-string">'TYPE'</span>, <span class="hljs-keyword">TYPE</span>,
            <span class="hljs-string">'NULLABLE'</span>, NULLABLE,
            <span class="hljs-string">'ORDER_ID'</span>, ORDER_ID
        )
    )
    <span class="hljs-keyword">WITHIN</span> <span class="hljs-keyword">GROUP</span> (<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> ORDER_ID)
    <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">TABLE</span>(INFER_SCHEMA(...))
);
</code></pre>
<p>Why would you want this? Schema evolution creates columns in UPPERCASE. If your initial columns are lowercase from the CSV header but new columns come in as uppercase, you end up with case mismatches. Normalizing upfront avoids the headache.</p>
<p><strong>Broadening numeric types:</strong></p>
<p>Here's the pattern I use now for every <code>INFER_SCHEMA</code> workflow:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> my_table
<span class="hljs-keyword">USING</span> <span class="hljs-keyword">TEMPLATE</span> (
    <span class="hljs-keyword">SELECT</span> ARRAY_AGG(
        OBJECT_CONSTRUCT(
            <span class="hljs-string">'COLUMN_NAME'</span>, <span class="hljs-keyword">UPPER</span>(COLUMN_NAME),
            <span class="hljs-string">'TYPE'</span>, <span class="hljs-keyword">CASE</span> 
                <span class="hljs-keyword">WHEN</span> <span class="hljs-keyword">REGEXP_LIKE</span>(<span class="hljs-keyword">TYPE</span>, <span class="hljs-string">'NUMBER\\([0-9]+,\\s?0\\)'</span>) <span class="hljs-keyword">THEN</span> <span class="hljs-string">'NUMBER(38, 0)'</span>
                <span class="hljs-keyword">WHEN</span> STARTSWITH(<span class="hljs-keyword">TYPE</span>, <span class="hljs-string">'NUMBER('</span>) <span class="hljs-keyword">THEN</span> <span class="hljs-string">'DOUBLE'</span>
                <span class="hljs-keyword">ELSE</span> <span class="hljs-keyword">TYPE</span>
            <span class="hljs-keyword">END</span>,
            <span class="hljs-string">'NULLABLE'</span>, NULLABLE,
            <span class="hljs-string">'ORDER_ID'</span>, ORDER_ID
        )
    )
    <span class="hljs-keyword">WITHIN</span> <span class="hljs-keyword">GROUP</span> (<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> ORDER_ID)
    <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">TABLE</span>(INFER_SCHEMA(...))
);
</code></pre>
<p>Let me break down that <code>CASE</code> statement:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Pattern</td><td>Transformation</td><td>Why</td></tr>
</thead>
<tbody>
<tr>
<td><code>NUMBER(X, 0)</code></td><td><code>NUMBER(38, 0)</code></td><td>Integers get minimal precision — <code>NUMBER(1,0)</code> for single digits. Broadening to <code>NUMBER(38,0)</code> handles any integer.</td></tr>
<tr>
<td><code>NUMBER(X, Y)</code></td><td><code>DOUBLE</code></td><td>Decimals like <code>NUMBER(3,2)</code> overflow with larger values. <code>DOUBLE</code> provides flexibility for real-world data.</td></tr>
<tr>
<td>Everything else</td><td>Keep as-is</td><td>Strings, timestamps, booleans don't have this problem.</td></tr>
</tbody>
</table>
</div><p>The regex <code>NUMBER\\([0-9]+,\\s?0\\)</code> matches integer types (scale of 0), while <code>STARTSWITH(TYPE, 'NUMBER(')</code> catches any remaining numeric types that have decimal places.</p>
<h2 id="heading-the-full-recipe">The Full Recipe</h2>
<p>Here's the complete pattern I use for CSV files:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create file format that reads headers</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FILE</span> <span class="hljs-keyword">FORMAT</span> my_csv_format
    <span class="hljs-keyword">TYPE</span> = <span class="hljs-string">'CSV'</span>
    PARSE_HEADER = <span class="hljs-literal">TRUE</span>
    ERROR_ON_COLUMN_COUNT_MISMATCH = <span class="hljs-literal">FALSE</span>;

<span class="hljs-comment">-- Create table with broadened types</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">TABLE</span> my_table
<span class="hljs-keyword">USING</span> <span class="hljs-keyword">TEMPLATE</span> (
    <span class="hljs-keyword">SELECT</span> ARRAY_AGG(
        OBJECT_CONSTRUCT(
            <span class="hljs-string">'COLUMN_NAME'</span>, <span class="hljs-keyword">UPPER</span>(COLUMN_NAME),
            <span class="hljs-string">'TYPE'</span>, <span class="hljs-keyword">CASE</span> 
                <span class="hljs-keyword">WHEN</span> <span class="hljs-keyword">REGEXP_LIKE</span>(<span class="hljs-keyword">TYPE</span>, <span class="hljs-string">'NUMBER\\([0-9]+,\\s?0\\)'</span>) <span class="hljs-keyword">THEN</span> <span class="hljs-string">'NUMBER(38, 0)'</span>
                <span class="hljs-keyword">WHEN</span> STARTSWITH(<span class="hljs-keyword">TYPE</span>, <span class="hljs-string">'NUMBER('</span>) <span class="hljs-keyword">THEN</span> <span class="hljs-string">'DOUBLE'</span>
                <span class="hljs-keyword">ELSE</span> <span class="hljs-keyword">TYPE</span>
            <span class="hljs-keyword">END</span>,
            <span class="hljs-string">'NULLABLE'</span>, NULLABLE,
            <span class="hljs-string">'ORDER_ID'</span>, ORDER_ID
        )
    )
    <span class="hljs-keyword">WITHIN</span> <span class="hljs-keyword">GROUP</span> (<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> ORDER_ID)
    <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">TABLE</span>(
        INFER_SCHEMA(
            LOCATION =&gt; <span class="hljs-string">'@my_stage/data/'</span>,
            FILE_FORMAT =&gt; <span class="hljs-string">'my_csv_format'</span>
        )
    )
)
ENABLE_SCHEMA_EVOLUTION = <span class="hljs-literal">TRUE</span>;
</code></pre>
<p>The <code>ENABLE_SCHEMA_EVOLUTION = TRUE</code> is critical if you want new columns to be added automatically — but that's a story for the next post.</p>
<h2 id="heading-what-about-parquet">What About Parquet?</h2>
<p>Good news: Parquet files have type information embedded in their metadata, so the inferred types are much more accurate. You typically don't need the numeric broadening trick.</p>
<p>But you should still uppercase the column names for consistency with schema evolution:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> my_table
<span class="hljs-keyword">USING</span> <span class="hljs-keyword">TEMPLATE</span> (
    <span class="hljs-keyword">SELECT</span> ARRAY_AGG(
        OBJECT_CONSTRUCT(
            <span class="hljs-string">'COLUMN_NAME'</span>, <span class="hljs-keyword">UPPER</span>(COLUMN_NAME),  <span class="hljs-comment">-- Still important!</span>
            <span class="hljs-string">'TYPE'</span>, <span class="hljs-keyword">TYPE</span>,
            <span class="hljs-string">'NULLABLE'</span>, NULLABLE,
            <span class="hljs-string">'ORDER_ID'</span>, ORDER_ID
        )
    )
    <span class="hljs-keyword">WITHIN</span> <span class="hljs-keyword">GROUP</span> (<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> ORDER_ID)
    <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">TABLE</span>(INFER_SCHEMA(...))
);
</code></pre>
<h2 id="heading-the-takeaway">The Takeaway</h2>
<p><code>INFER_SCHEMA()</code> is fantastic for prototyping and development. But for production, don't trust it blindly — post-process the template using SQL functions to broaden numeric types and normalize column names. Your future self (and your pipelines) will thank you.</p>
<hr />
<p><em>Next up: How to combine this pattern with Snowpipe for automatic schema evolution. Stay tuned.</em></p>
]]></content:encoded></item><item><title><![CDATA[Inferring Schema from VARIANT Fields in Snowflake]]></title><description><![CDATA[I get asked about this a lot. Someone lands JSON from a REST API into a VARIANT column, and now they want proper columns without manually writing json_data:field1::VARCHAR, json_data:field2::NUMBER for every single path. And you guessed it - there's ...]]></description><link>https://anthu.dev/snowflake-variant-schema-inference</link><guid isPermaLink="true">https://anthu.dev/snowflake-variant-schema-inference</guid><category><![CDATA[snowflake]]></category><category><![CDATA[SQL]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[json]]></category><dc:creator><![CDATA[Anton Huck]]></dc:creator><pubDate>Tue, 13 Jan 2026 16:01:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/CFKwL570ZSc/upload/3b1eead5bde46634178b3e0cb14b37c0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I get asked about this a lot. Someone lands JSON from a REST API into a VARIANT column, and now they want proper columns without manually writing <code>json_data:field1::VARCHAR</code>, <code>json_data:field2::NUMBER</code> for every single path. And you guessed it - there's no built-in <code>INFER_SCHEMA</code> for VARIANT columns like there is for staged files.</p>
<p>The reason? It's genuinely hard. Unlike staged files where Snowflake can sample a few files upfront, VARIANT columns can contain wildly different structures across rows, nested objects go arbitrarily deep, and arrays make things exponentially messier. So far so good - but that doesn't mean we can't build something ourselves.</p>
<h2 id="heading-the-core-trick-recursive-flatten-typeof">The Core Trick: Recursive FLATTEN + TYPEOF</h2>
<p>Here's the discovery query that makes everything possible:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">DISTINCT</span>
    f.path <span class="hljs-keyword">AS</span> original_path,
    <span class="hljs-keyword">UPPER</span>(<span class="hljs-keyword">REPLACE</span>(f.path, <span class="hljs-string">'.'</span>, <span class="hljs-string">'_'</span>)) <span class="hljs-keyword">AS</span> column_name,
    TYPEOF(f.value) <span class="hljs-keyword">AS</span> data_type
<span class="hljs-keyword">FROM</span> my_table,
<span class="hljs-keyword">LATERAL</span> FLATTEN(json_data, <span class="hljs-keyword">RECURSIVE</span> =&gt; <span class="hljs-literal">TRUE</span>) f
<span class="hljs-keyword">WHERE</span> TYPEOF(f.value) <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">IN</span> (<span class="hljs-string">'OBJECT'</span>)
  <span class="hljs-keyword">AND</span> f.path <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">LIKE</span> <span class="hljs-string">'%[%'</span>
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> column_name;
</code></pre>
<p><code>FLATTEN</code> with <code>RECURSIVE =&gt; TRUE</code> walks the entire JSON tree and returns every path. <code>TYPEOF()</code> tells us what's at each path. The <code>NOT LIKE '%[%'</code> filter excludes array contents - I'll explain why in a moment.</p>
<p>Run this against a typical API response and you'll see something like:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>original_path</td><td>column_name</td><td>data_type</td></tr>
</thead>
<tbody>
<tr>
<td>address.city</td><td>ADDRESS_CITY</td><td>VARCHAR</td></tr>
<tr>
<td>address.geo.lat</td><td>ADDRESS_GEO_LAT</td><td>DOUBLE</td></tr>
<tr>
<td>balance</td><td>BALANCE</td><td>DECIMAL</td></tr>
<tr>
<td>is_active</td><td>IS_ACTIVE</td><td>BOOLEAN</td></tr>
<tr>
<td>orders</td><td>ORDERS</td><td>ARRAY</td></tr>
<tr>
<td>tags</td><td>TAGS</td><td>ARRAY</td></tr>
</tbody>
</table>
</div><h2 id="heading-why-arrays-stay-as-variant">Why Arrays Stay as VARIANT</h2>
<p>My first instinct was to recursively explode everything. But that creates a mess:</p>
<ol>
<li><p>Arrays can have different lengths per row - do you create <code>ITEM_0</code>, <code>ITEM_1</code>, <code>ITEM_2</code>... how many?</p>
</li>
<li><p>Nested arrays explode your row count exponentially</p>
</li>
<li><p>The resulting schema becomes unpredictable</p>
</li>
</ol>
<p>Instead, the approach I landed on keeps top-level arrays as VARIANT columns. You can still query them with <code>LATERAL FLATTEN</code> when you need to, but your base schema stays stable. The people who need to dig into arrays can handle that downstream.</p>
<h2 id="heading-wrapping-it-in-a-procedure">Wrapping It in a Procedure</h2>
<p>Once you've got the discovery query working, wrapping it in a stored procedure makes it reusable:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">PROCEDURE</span> discover_json_schema(
    source_table <span class="hljs-built_in">VARCHAR</span>,
    variant_column <span class="hljs-built_in">VARCHAR</span>
)
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">ARRAY</span>
<span class="hljs-keyword">LANGUAGE</span> <span class="hljs-keyword">SQL</span>
<span class="hljs-keyword">AS</span>
<span class="hljs-keyword">DECLARE</span>
    schema_array <span class="hljs-built_in">ARRAY</span>;
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">SELECT</span> ARRAY_AGG(OBJECT_CONSTRUCT(
        <span class="hljs-string">'original_path'</span>, original_path,
        <span class="hljs-string">'column_name'</span>, column_name,
        <span class="hljs-string">'sql_type'</span>, sql_type
    ))
    <span class="hljs-keyword">INTO</span> schema_array
    <span class="hljs-keyword">FROM</span> (
        <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">DISTINCT</span>
            f.path <span class="hljs-keyword">AS</span> original_path,
            <span class="hljs-keyword">UPPER</span>(<span class="hljs-keyword">REPLACE</span>(f.path, <span class="hljs-string">'.'</span>, <span class="hljs-string">'_'</span>)) <span class="hljs-keyword">AS</span> column_name,
            <span class="hljs-keyword">CASE</span>
                <span class="hljs-keyword">WHEN</span> TYPEOF(f.value) = <span class="hljs-string">'INTEGER'</span> <span class="hljs-keyword">THEN</span> <span class="hljs-string">'NUMBER'</span>
                <span class="hljs-keyword">WHEN</span> TYPEOF(f.value) <span class="hljs-keyword">IN</span> (<span class="hljs-string">'DOUBLE'</span>, <span class="hljs-string">'DECIMAL'</span>) <span class="hljs-keyword">THEN</span> <span class="hljs-string">'FLOAT'</span>
                <span class="hljs-keyword">WHEN</span> TYPEOF(f.value) = <span class="hljs-string">'BOOLEAN'</span> <span class="hljs-keyword">THEN</span> <span class="hljs-string">'BOOLEAN'</span>
                <span class="hljs-keyword">WHEN</span> TYPEOF(f.value) = <span class="hljs-string">'ARRAY'</span> <span class="hljs-keyword">THEN</span> <span class="hljs-string">'VARIANT'</span>
                <span class="hljs-keyword">ELSE</span> <span class="hljs-string">'VARCHAR'</span>
            <span class="hljs-keyword">END</span> <span class="hljs-keyword">AS</span> sql_type
        <span class="hljs-keyword">FROM</span> IDENTIFIER(:source_table),
        <span class="hljs-keyword">LATERAL</span> FLATTEN(IDENTIFIER(:variant_column), <span class="hljs-keyword">RECURSIVE</span> =&gt; <span class="hljs-literal">TRUE</span>) f
        <span class="hljs-keyword">WHERE</span> TYPEOF(f.value) <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">IN</span> (<span class="hljs-string">'OBJECT'</span>)
          <span class="hljs-keyword">AND</span> f.path <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">LIKE</span> <span class="hljs-string">'%[%'</span>
        <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> column_name
    );

    RETURN schema_array;
<span class="hljs-keyword">END</span>;
</code></pre>
<p>Call it like this:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CALL</span> discover_json_schema(<span class="hljs-string">'my_db.my_schema.raw_api_data'</span>, <span class="hljs-string">'json_data'</span>);
</code></pre>
<p>Returns an array of objects you can loop through to generate DDL.</p>
<pre><code class="lang-json">[
  {
    <span class="hljs-attr">"column_name"</span>: <span class="hljs-string">"EMAIL"</span>,
    <span class="hljs-attr">"original_path"</span>: <span class="hljs-string">"email"</span>,
    <span class="hljs-attr">"sql_type"</span>: <span class="hljs-string">"VARCHAR"</span>
  },
  {
    <span class="hljs-attr">"column_name"</span>: <span class="hljs-string">"IS_ACTIVE"</span>,
    <span class="hljs-attr">"original_path"</span>: <span class="hljs-string">"is_active"</span>,
    <span class="hljs-attr">"sql_type"</span>: <span class="hljs-string">"BOOLEAN"</span>
  },
-- [... and so on ]
]
</code></pre>
<h2 id="heading-generating-views-automatically">Generating Views Automatically</h2>
<p>The natural next step - a procedure that creates a view with all discovered columns:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">PROCEDURE</span> generate_flattened_view(
    source_table <span class="hljs-built_in">VARCHAR</span>,
    variant_column <span class="hljs-built_in">VARCHAR</span>,
    target_view <span class="hljs-built_in">VARCHAR</span>
)
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">VARCHAR</span>
<span class="hljs-keyword">LANGUAGE</span> <span class="hljs-keyword">SQL</span>
<span class="hljs-keyword">AS</span>
<span class="hljs-keyword">DECLARE</span>
    ddl_statement <span class="hljs-built_in">VARCHAR</span>;
    select_cols VARCHAR;
    schema_array ARRAY;
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">CALL</span> discover_json_schema(:source_table, :variant_column) <span class="hljs-keyword">INTO</span> schema_array;

    <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">LISTAGG</span>(
        <span class="hljs-string">'GET_PATH('</span> || :variant_column || <span class="hljs-string">', '''</span> || s.value:original_path::<span class="hljs-built_in">VARCHAR</span> || <span class="hljs-string">''')::'</span> ||
        s.value:sql_type::<span class="hljs-built_in">VARCHAR</span> || <span class="hljs-string">' AS "'</span> || s.value:column_name::<span class="hljs-built_in">VARCHAR</span> || <span class="hljs-string">'"'</span>,
        <span class="hljs-string">', '</span>
    ) <span class="hljs-keyword">WITHIN</span> <span class="hljs-keyword">GROUP</span> (<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> s.value:column_name::<span class="hljs-built_in">VARCHAR</span>)
    <span class="hljs-keyword">INTO</span> select_cols
    <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">TABLE</span>(FLATTEN(:schema_array)) s;

    ddl_statement := '<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">VIEW</span> <span class="hljs-string">' || :target_view ||
                     '</span> <span class="hljs-keyword">AS</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-string">' || select_cols ||
                     '</span> <span class="hljs-keyword">FROM</span> <span class="hljs-string">' || :source_table;
    EXECUTE IMMEDIATE ddl_statement;

    RETURN '</span>Created <span class="hljs-keyword">view</span>: <span class="hljs-string">' || :target_view;
END;</span>
</code></pre>
<p>Now one call flattens your entire JSON structure:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CALL</span> generate_flattened_view(
    <span class="hljs-string">'raw_api_data'</span>,
    <span class="hljs-string">'json_data'</span>,
    <span class="hljs-string">'api_data_flat'</span>
);
</code></pre>
<h2 id="heading-dynamic-tables-for-auto-refresh">Dynamic Tables for Auto-Refresh</h2>
<p>Same idea, but with Dynamic Tables for continuously refreshing data:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">PROCEDURE</span> generate_flattened_dynamic_table(
    source_table <span class="hljs-built_in">VARCHAR</span>,
    variant_column <span class="hljs-built_in">VARCHAR</span>,
    target_dt <span class="hljs-built_in">VARCHAR</span>,
    warehouse <span class="hljs-built_in">VARCHAR</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'COMPUTE_WH'</span>,
    target_lag <span class="hljs-built_in">VARCHAR</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'1 hour'</span>
)
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">VARCHAR</span>
<span class="hljs-keyword">LANGUAGE</span> <span class="hljs-keyword">SQL</span>
<span class="hljs-keyword">AS</span>
<span class="hljs-keyword">DECLARE</span>
    ddl_statement <span class="hljs-built_in">VARCHAR</span>;
    select_cols VARCHAR;
    schema_array ARRAY;
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">CALL</span> discover_json_schema(:source_table, :variant_column) <span class="hljs-keyword">INTO</span> schema_array;

    <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">LISTAGG</span>(
        <span class="hljs-string">'GET_PATH('</span> || :variant_column || <span class="hljs-string">', '''</span> || s.value:original_path::<span class="hljs-built_in">VARCHAR</span> || <span class="hljs-string">''')::'</span> ||
        s.value:sql_type::<span class="hljs-built_in">VARCHAR</span> || <span class="hljs-string">' AS "'</span> || s.value:column_name::<span class="hljs-built_in">VARCHAR</span> || <span class="hljs-string">'"'</span>,
        <span class="hljs-string">', '</span>
    ) <span class="hljs-keyword">WITHIN</span> <span class="hljs-keyword">GROUP</span> (<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> s.value:column_name::<span class="hljs-built_in">VARCHAR</span>)
    <span class="hljs-keyword">INTO</span> select_cols
    <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">TABLE</span>(FLATTEN(:schema_array)) s;

    ddl_statement := '<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> DYNAMIC <span class="hljs-keyword">TABLE</span> <span class="hljs-string">' || :target_dt ||
                     '</span> TARGET_LAG = <span class="hljs-string">''' || :target_lag || '''' ||
                     '</span> WAREHOUSE = <span class="hljs-string">' || :warehouse ||
                     '</span> <span class="hljs-keyword">AS</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-string">' || select_cols ||
                     '</span> <span class="hljs-keyword">FROM</span> <span class="hljs-string">' || :source_table;
    EXECUTE IMMEDIATE ddl_statement;

    RETURN '</span>Created dynamic <span class="hljs-keyword">table</span>: <span class="hljs-string">' || :target_dt;
END;</span>
</code></pre>
<h2 id="heading-caveats">Caveats</h2>
<p>This is a starting point, not a production-ready solution. Things you'll likely need to adjust:</p>
<ul>
<li><p><strong>Type conflicts</strong>: If the same path has different types across rows (eg. APIs returning <code>null</code> vs <code>0</code>), the discovery picks one. You might want majority-wins logic or explicit overrides.</p>
</li>
<li><p><strong>Column name collisions</strong>: <code>user.id</code> and <code>user_id</code> both become <code>USER_ID</code>. Add disambiguation if your data has this. For example you can use a different separator.</p>
</li>
<li><p><strong>Schema evolution</strong>: New fields in source JSON won't automatically appear. Re-run the procedure or build a scheduled task to detect drift.</p>
</li>
<li><p><strong>Performance</strong>: Scanning the entire table for schema discovery is expensive. Consider sampling with <a target="_blank" href="https://docs.snowflake.com/en/sql-reference/constructs/sample"><code>TABLESAMPLE</code></a> or <a target="_blank" href="https://docs.snowflake.com/en/sql-reference/constructs/limit"><code>LIMIT</code></a> for large tables.</p>
</li>
</ul>
<h2 id="heading-when-to-use-what">When to Use What</h2>
<ul>
<li><p><strong>One-off exploration</strong>: Run the discovery query directly, eyeball the results</p>
</li>
<li><p><strong>Stable schema, needs to stay current</strong>: Generate a View</p>
</li>
<li><p><strong>Performance-critical queries on semi-structured data</strong>: Generate a Dynamic Table</p>
</li>
<li><p><strong>Complex transformation logic</strong>: Use the Python variant of the procedure and add your business rules</p>
</li>
</ul>
<p>The full notebook with all procedures and sample data is available - drop me a line if you want it.</p>
]]></content:encoded></item><item><title><![CDATA[Random Numbers in Tableau]]></title><description><![CDATA[Having a lot of uniform data you might want to introduce some jitter to the data visualization.
Most of the tutorials would suggest to use a random number and apply it to an axis to offset the visualistion point a bit. So far so good.
Tableau for spe...]]></description><link>https://anthu.dev/random-function-in-tableau</link><guid isPermaLink="true">https://anthu.dev/random-function-in-tableau</guid><category><![CDATA[tableau]]></category><category><![CDATA[BI]]></category><dc:creator><![CDATA[Anton Huck]]></dc:creator><pubDate>Mon, 10 Jan 2022 11:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768514930225/897c5378-a168-4313-9fbd-6e3b07a0f77c.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Having a lot of uniform data you might want to introduce some jitter to the data visualization.</p>
<p>Most of the tutorials would suggest to use a random number and apply it to an axis to offset the visualistion point a bit. So far so good.</p>
<p>Tableau for special had a hidden function called <code>RANDOM()</code> for quite some while which got removed recently. Their forums are full of requests to bring it back without much response from what I see.</p>
<p>On my search for a workaround I found following solution for Snowflake (should work for every DB, though):</p>
<pre><code class="lang-SQL">// The easiest way
RAWSQL_INT("random(42)")

// Uniform the random number to be between 0 and 1
RAWSQL_REAL("uniform(0::float, 1::float, random(42)")
</code></pre>
<p>The idea is quite straight forward: I'm simply using the <code>random()</code> function of my datasource. In case of Snowflake I suggest to use a seeded random function what will prevent "jumping" datapoints after each reload (In my case it's seed "42").</p>
<p>In case you need a constrained random number, simply use the Snowflake <code>uniform()</code> function to map the random numbers to your needs.</p>
<p>This works for non-extracted Data-Sources. For everything else, I'll post a Pseudo-Random-Number-Generator code shortly. Interested? Subscribe to my Blog or drop me a line on Twitter.</p>
]]></content:encoded></item><item><title><![CDATA[Finding a lost Apple Pencil using sysdiagnose]]></title><description><![CDATA[I've always been a fan of Apple's iCloud Find My. Not only because I tend to misplace my keys or my Phone but the overall experience of the ecosystem. But lastly I had to learn the limitations the hard way. You can't use Find My for all Apple devices...]]></description><link>https://anthu.dev/finding-lost-apple-pencil</link><guid isPermaLink="true">https://anthu.dev/finding-lost-apple-pencil</guid><category><![CDATA[Apple]]></category><dc:creator><![CDATA[Anton Huck]]></dc:creator><pubDate>Sun, 26 Dec 2021 11:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768514782593/72ae2a74-3c3b-4038-bb19-7dc55a7fcacb.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've always been a fan of Apple's <a target="_blank" href="https://www.apple.com/icloud/find-my/">iCloud Find My</a>. Not only because I tend to misplace my keys or my Phone but the overall experience of the ecosystem. But lastly I had to learn the limitations the hard way. You can't use Find My for all Apple devices and they excluded one of the probably most lost devices: The Apple Pencil. I had to be creative to come up with an approximation, where to find a lost one. Read about it in this post.</p>
<h2 id="heading-the-mission">The Mission</h2>
<p>A few days back my sister told me that she lost her Apple Pencil at the University. My naive answer was (being the Find My guy): check the Find My App and you should see the last location. Unfortunately she couldn't find the Pencil in her app. Double-checking the <a target="_blank" href="https://www.apple.com/icloud/find-my/">Find My landing page</a> she seems to be right - Apple Pencil is not supported by the app.</p>
<p>My first thoughts were: well, it's almost Christmas, but my second thought was about helping to find her belonging.</p>
<p>There are at least the following approaches to find a lost Bluetooth device:1. Searching where it got lost2. (paid) Apps3. Try Bluetooth pairing (maybe the device is still around)4. <strong>Digging through logs</strong></p>
<p>In this post I'll discuss all of them but will focus on the last one.</p>
<h2 id="heading-the-discovery">The Discovery</h2>
<p>Let's start with the obvious one: Searching where it was lost.</p>
<p>My sister was pretty sure that she left her Pencil at the University. It was the obvious guess: she used it there the day before and it was the last time she saw it. After calling back and forth everybody around that class room she finally gave up and admitted the loss.</p>
<p>The next day she got some new hope and searched Google for a way to uncover the last location. And you guessed it - Google is full of Ads for Bluetooth tracking Apps which claim to be the best around. They will notify you on a potential leave behind of a tracked peripheral. Well, it's working <strong>as long as you're "planning ahead"</strong> but none of them will recover the last seen location of an already lost device. What a bummer.</p>
<p>Later that day she approached me with the sad news - so I did the same: Searching the web. My first hits were some geek-websites suggesting "walking around" and wait for a connection - expecting their audience not leaving their home and therefore losing stuff in a bluetooth range. The rest of the results was pretty much the same as already discussed earlier.</p>
<h2 id="heading-the-improvisation">The Improvisation</h2>
<p>As I worked in IT support / Systems Engineering quite some time - one of the first things that I tend to do when anything is broken or lost is checking the logs (or more broadly any of the <a target="_blank" href="https://www.oreilly.com/library/view/distributed-systems-observability/9781492033431/ch04.html">three pillars</a>). Usually you can just connect your iPhone to your Mac, trust the computer and use the Console.App to search trough the logs.</p>
<p>Well, getting the logs of an iPad OS running device that is not anywhere around (my sister is not that tech-savvy and living some hundred kilometers away) turned out to be more tricky than just plugging in and using the Mac Console. Luckily the Apple developer forums are not that extensive but clear enough <a target="_blank" href="https://developer.apple.com/forums/thread/80811">to learn about sysdiagnose</a>.</p>
<p>For my devices running the latest iOS 15 it was straight forward to trigger a sysdiagnose snapshot, holding all physical keys for a few milliseconds:</p>
<pre><code class="lang-plaintext">[Volume Up] + [Volume Down] + [Power]
</code></pre>
<p>It's really less than a second - <strong>don't expect any feedback</strong> (sometimes you hit a screenshot).</p>
<p>Then there is a crucial part for all of us impatient people: <strong>Wait a minute or two!</strong> You will be rewarded with an archive of significant size to explore. Find it in the privacy settings:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768514796386/4376e265-8e13-4e5e-95fe-8ba33b3dd943.png" alt class="image--center mx-auto" /></p>
<p>Notice the timestamp? - Click on the latest report and share it to your Mac - I had to use iCloud as AirDrop was no option due to the distance.</p>
<p>Once the archive is loaded to your Mac - unpack (double click does the Apple-Magic!). Let's have a look what's in there:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768514809096/8425c2dd-9d7e-4ce5-bc75-f2300f7b1880.png" alt class="image--center mx-auto" /></p>
<p>That's sweet - a bunch of diagnostic reports to dig through. <strong>Take your time and look around on your own</strong>. Spoiler: for our next step the <code>logs</code> folder won't be as helpful as I initially expected.</p>
<h2 id="heading-the-final-sprint">The Final Sprint</h2>
<p>I probably did the same as you, clicking through all the folders and peeking into the files. After playing around with <code>grep</code>, the <code>system_logs.logarchive</code> surfaced as the most promissing with a lot of hits for the keyword <code>Apple Pencil</code> :</p>
<pre><code class="lang-bash">grep -irFe <span class="hljs-string">"Apple Pencil"</span>
</code></pre>
<p><img src="https://anthu.dev/content/images/2021/12/image-3.png" alt /></p>
<p>But how to read these binary files? Obviously you can load them into the Built-in Mac tool "Console" (it has nothing to do with the terminal, it's rather a tool to read the logs from the Mac or connected Apple Devices). For whatever reason the imported archive did not show up any <code>Apple Pencil</code> matches within the Console although <code>grep</code> found something.</p>
<p>Last but not least I found the <code>log</code> command which is also delivered out of the box and presumably the CLI for the Console:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768514823837/6545b293-58c7-4b92-9db5-2762944fe1cb.png" alt class="image--center mx-auto" /></p>
<p>Great! Let's put it together and find the lost Apple Pencil:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">log</span> show --archive system_logs.logarchive | grep -iF <span class="hljs-string">"Apple Pencil"</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768514851896/9b032839-ad2d-4753-a5b7-f417ce377911.jpeg" alt class="image--center mx-auto" /></p>
<p>At this point look for pattern changes. You should see <strong>disconnect, power found and loss events</strong> - these events will hint you the last-seen-time.</p>
<p>Unfortunately this approach won't give you a location, but you will give a quite accurate timestamp when to look back for it. In the case of my sister we were able to combine the timestamp with her Google Maps Location History and pinpoint the loss-location. To both of our surprise it wasn't the University but hours later at a public parking lot.</p>
<p>Even if you don't have Google Location History you might be able to pinpoint the location, too - look at your chats, emails, calls &amp; be creative! But if you do have Google Maps Location History - be curious about the upcoming posts where I will go through the Location History and use Google Takeout to get a better understanding what Google is saving about you.</p>
]]></content:encoded></item><item><title><![CDATA[Azure DevOps: Get short commit hash in build pipeline]]></title><description><![CDATA[When building potentially deployable artifacts as part of the build pipeline it’s crutical to identify each produced artifact based on the source of the build. Helpful tracers I’m using are branch name, commit hash and the build number. In my current...]]></description><link>https://anthu.dev/azure-devops-short-hash</link><guid isPermaLink="true">https://anthu.dev/azure-devops-short-hash</guid><category><![CDATA[azure-devops]]></category><dc:creator><![CDATA[Anton Huck]]></dc:creator><pubDate>Tue, 27 Jul 2021 10:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768514978521/f110a150-5dbf-40c2-b030-6557d48b995b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>When building potentially deployable artifacts as part of the build pipeline it’s crutical to identify each produced artifact based on the source of the build. Helpful tracers I’m using are branch name, commit hash and the build number. In my current project I’m using Azure DevOps Pipelines for building and they provide a lot of helpful predefined variables (find a full list</strong> <a target="_blank" href="https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&amp;tabs=yaml"><strong>here</strong></a><strong>). Anyway there is one variable I’m missing: short commit hash.</strong></p>
<p>Of course Microsoft could provide this variable out of the box but I also get the point that requirements are highly different. While I prefer the 8-char long hash other might be happy with seven or ten characters. I will show you that it’s not that complicated to trim your own commit hash and share the snippet I’m using for almost every pipeline:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">powershell:</span> <span class="hljs-string">|
    $shortHash = $env:BUILD_SOURCEVERSION.subString(0, 7)
    Write-Host "##vso[task.setvariable variable=shortHash]$shortHash"</span>
</code></pre>
<p>In bash it’s even easier:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">powershell:</span> <span class="hljs-string">|</span>
    <span class="hljs-string">echo</span> <span class="hljs-string">"##vso[task.setvariable variable=shortHash]${BUILD_SOURCEVERSION:0:7}"</span>
</code></pre>
<p>And this is how you can use it in you pipeline later (remember that the template experession syntax <code>${{shortHash}}</code> won’t work as the variable needs to be evaluated at runtime)</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">DotNetCoreCLI@2</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">Publish</span> <span class="hljs-string">.NET</span> <span class="hljs-string">Application</span>
  <span class="hljs-attr">inputs:</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">'publish'</span>
    <span class="hljs-attr">arguments:</span> <span class="hljs-string">'--configuration '</span><span class="hljs-string">Release'</span> <span class="hljs-string">--output</span> <span class="hljs-string">$(Build.ArtifactStagingDirectory)</span> <span class="hljs-string">--version-suffix</span> <span class="hljs-string">$(shortHash)-$(Build.BuildNumber)'</span>
</code></pre>
]]></content:encoded></item></channel></rss>