.
Here's where the story gets chosen—a single window function, and no LLM has been called yet, nor will be until this has picked exactly one:
CREATE OR REPLACE VIEW REPO_STORYLINE AS
SELECT
f.REPO_OWNER, f.REPO_NAME,
COALESCE(s.STORYLINE, 'none') AS STORYLINE,
COALESCE(s.SCORE, 0) AS SCORE,
s.PIVOT_AT,
-- every fact the card will ever print, computed here, in SQL
OBJECT_CONSTRUCT(
'commitCount', f.COMMIT_COUNT,
'nightCommits', f.NIGHT_COMMITS,
'activeDays', f.ACTIVE_DAYS,
'daysSinceLast', f.DAYS_SINCE_LAST,
'largestGap', OBJECT_CONSTRUCT('days', g.GAP_DAYS, ...)
) AS FACTS
FROM REPO_FACTS f
LEFT JOIN REPO_LARGEST_GAP g USING (REPO_OWNER, REPO_NAME)
LEFT JOIN STORYLINE_SCORES s USING (REPO_OWNER, REPO_NAME)
QUALIFY ROW_NUMBER() OVER (
PARTITION BY f.REPO_OWNER, f.REPO_NAME
ORDER BY s.SCORE DESC NULLS LAST, s.DRAMA_RANK -- ties break toward drama
) = 1;
Every storyline gates on MIN_COMMITS = 15, so bot noise can't win. Scoring is deterministic: the same repo always yields the same template.
And the tiebreak in CARD_EVIDENCE, which is the difference between a card and a card that changes its mind:
-- Rebases and batch pushes share an AUTHORED_AT. Without SHA as the final
-- tiebreak, the commits Cortex sees could differ between two reads of the
-- same repo — and the card would quietly rewrite itself.
ORDER BY ABS(DATEDIFF(hour, w.PIVOT_AT, c.AUTHORED_AT)), c.AUTHORED_AT, c.SHA
How I Built It
Snowflake is the prize tech, and it's also the whole engine—ingest, detection, narration, and validation all run inside the warehouse. Everything below is how.
My entire API surface is one line:
CALL READ_REPO('anchildress1', 'save-the-sun');
There is no application logic deciding the story. Snowflake decides the story. My backend has never read one of your commit messages and would not know what to do with one.
1. Why every layer narrows 🪙
I already used my free Snowflake trial. Cortex is coming out of my pocket.
So the architecture has exactly one obsession of its own—give the model as little as possible and still get a story back—and every slice, cap, floor, and filter here exists because I am personally paying for the tokens on the other side of it.
Which is not a compromise. Cheap scales. Expensive doesn't. I have been doing this long enough to know that a per-request cost you can't bound is a system with its tombstone already etched, and the fastest way to build something that holds up under real traffic is to build it as though every call is coming out of your own account—because eventually, for someone, it is.
So the bound is the feature. The model's input is 20 to 140 lines. Always. A repo with twenty thousand commits and one with two hundred have the same maximum narration cost—the expensive call doesn't grow with your history, so the version running on my card and the version running for ten thousand people are the same architecture. I don't have to rewrite it later.
2. Snowflake goes and gets its own data 🛰️
An EXTERNAL ACCESS INTEGRATION lets a Python stored procedure call api.github.com from inside the warehouse, which means there is no ingestion service, no ETL job, and no Cloud Function in the middle holding a copy of your commits.
Object |
Type |
Job |
|---|
GITHUB_API_RULE |
NETWORK RULE (EGRESS) |
Lets the warehouse out to api.github.com
|
GITHUB_TOKEN |
SECRET |
The token, created out-of-band |
GITHUB_API_ACCESS |
EXTERNAL ACCESS INTEGRATION |
Binds the rule to the secret |
INGEST_REPO_COMMITS |
PROCEDURE (Python) |
Paginates the Commits API into COMMITS, then classifies bot and AI-assisted rows in SQL |
Ingest caps at 500 commits, which is the first cut and the first thing standing between a monorepo and my bill. A longer history sets windowed, and the card prints it—"last 500 commits · quiet since Feb 25"—because a cap you hide is a lie, and reporting a slice as a repo's whole life is false.
This is the one place a model runs before the story is chosen, and it barely runs at all. Regex and GitHub's own account type settle roughly 99% of the bot question. Only the genuine ambiguities—a human-looking account committing like a machine, a subject line that mentions an AI tool without being written by one—get handed to AI_CLASSIFY and AI_FILTER, deduped by (author, email) so it's one call per distinct identity rather than one per commit. No candidate, no call.
3. The detector is free 💸
Scoring six storylines across a repo's whole history costs nothing but warehouse seconds—not one model call in fifteen views—and that layer is what makes the expensive layer cheap.
By the time a model is involved, SQL has already dropped the merges and the bots, scored every candidate narrative, picked exactly one winner, and selected the commit lines belonging to that thread only.
Snowflake could hand the model your entire repo without breaking a sweat. It doesn't have to, so it doesn't. The warehouse decides what's worth reading before a single token gets spent, which is the difference between a bill that scales with a repo and one that doesn't.
4. The model is a SQL function 🔬
CHRONICLE_CARD is a hand-written UDF wrapping AI_COMPLETE (claude-sonnet-4-5), and here's the part that matters: the model is invoked from inside a SELECT.
There is no HTTP call, no SDK, no retry wrapper, no queue of prompts, and no service account carrying an API key. It's a function, in a query, sitting next to the rows it reads. The evidence never leaves the warehouse to get narrated, and the narration lands back in a table on the way out.
The prompt is built in SQL too—string concatenation, inside the UDF, from the arguments READ_REPO hands it. So the only non-deterministic step in this entire pipeline is the sentence, and there is deterministic SQL standing on both sides of it: SQL computes the facts, picks the storyline, selects the evidence, and writes the prompt; the model writes prose; SQL then validates what came back before any of it reaches a card.
I prototyped it in Cortex AI Function Studio and then wrote it out as a plain UDF, so the function lives in the repo and deploys with the snow CLI—a function clicked into existence in a UI doesn't live in your git history.
It runs at temperature: 0.4, on purpose. I started at zero, because zero is the responsible number, and zero was boring—the prose came back correct and dead. So I turned it up until the writing had a pulse and made the warehouse carry the safety instead of the sampler. The story selection is deterministic; the sentence isn't.
It's fed CARD_EVIDENCE: the winning thread's commit lines, budgeted at 25% of the repo's commit lines, floored at 20, capped at 140. That cap is the invoice—the only number in this project I tuned with a calculator instead of taste.
Squash-merge bodies get exploded into individual lines first, so work buried inside a merge is still readable.
Cortex is never taught to produce a number. The commit count, the status verb, the anchor timestamps, the gap panel, the caption—all of it is composed by the renderer, from facts SQL already computed.
That rule came from a real failure. Handed the facts as one JSON blob, the model wrote "fifty-six commits after midnight" about a repo with fifty-six commits in total and forty-seven at night. It read an adjacent integer and captioned it wrong. Now every fact arrives as its own labelled argument, and the model isn't allowed near a digit.
So the schema constrains exactly nine keys, and that is the entire surface area of the writing. Here's the real row out of CARDS for the card up top:
{
"kicker": "a graveyard shift",
"headline_upright": "Fifty-six percent of it happened",
"headline_accent": "after midnight",
"headline_trail": ".",
"label_first": "the first small hour",
"label_pivot": "",
"label_last": "",
"accent": "#6ab5f5",
"accent_reason": "sky, for a project that lived in the dark — more than half its commits came between midnight and dawn"
}
Not one digit in there. "Fifty-six percent" is a share the detector handed it, spelled out as words, and a graveyard shift is a phrase that appears nowhere in the prompt, the schema, or the storyline names. The model got twenty-three timestamps and read them.
5. SQL verifies the model before the card exists 🛡️
A warmer sampler gets you room to be wrong in new ways, so nothing the model says is trusted until SQL has been through it.
Constrained decoding returns NULL when the model hits max_tokens or the schema rejects a draft, and a NULL looks exactly like an outage. READ_REPO checks the output before it stores anything:
Empty-response guard → cortex_empty. max_tokens sits at 2048, deliberately generous, because a stingy budget is indistinguishable from a real failure.
Reject guard → cortex_rejected. A malformed accent hex, a digit smuggled into a poetic label, or a kicker that just echoes the storyline name back at me—"nocturne" is the input, not an answer.
A rejected card fails loudly, with reasons. It never renders a lie. And every one of those checks is SQL, so catching a bad card costs me nothing.
6. Everything else is me protecting the bank 🪟
The remaining architecture is one long argument with my own bank statement:
The queue is a cost decision, not plumbing. The pipeline runs on a Cloud Tasks worker request that calls back into the service. Detaching work from the originating request would need Cloud Run's --no-cpu-throttling, which bills instance time instead of request time, and you pay for a container to sit there doing nothing. This way you can close the tab and I don't buy idle CPU.
The bucket is the cache of record. The card's existence in it is the ready state. No Firestore, no status column, no second database to pay for. card.json is written last, so a crash leaves a job retryable and never leaves a "ready" card that doesn't exist.
A create-only claim means two people hammering the same repo can't both bill a Cortex call.
Failures are cached, so a dead repo can't charge me twice for the same bad news.
A none verdict skips Cortex entirely. The grey card is free.
Every boundary, in one table 📐
None of these are benchmarks. They're the walls—the numbers that decide what this thing is allowed to do to my account:
Boundary |
Value |
Why it exists |
|---|
Ingest cap |
500 commits (hard ceiling 2,000) |
A monorepo can't run away with the bill, and windowed cards print the fact |
Evidence budget |
25% of the repo's commit lines, min 20, max 140 |
The invoice. The only number the model's cost scales with |
Cortex calls per card |
1 |
Nine fields, one round trip, max_tokens: 2048
|
Storyline floor |
MIN_COMMITS = 15 |
Bot noise can't win a story it didn't earn |
Detector cost |
0 AI calls, 15 views |
Six narratives scored in plain SQL. The only model that runs before this is the bot classifier, on ambiguities only |
Reject checks |
13, in SQL |
Free to run, and a bad card never reaches the table |
Warehouse |
XSMALL, AUTO_SUSPEND = 60s, STATEMENT_TIMEOUT = 300s |
It runs when there's work and stops when there isn't |
Daily generations |
capped, counted in the bucket |
Holds across instances, so scale-out can't bypass it |
Queue concurrency |
2 |
A ceiling on how fast anyone can spend my money |
Audit |
CORTEX_QUERY_ID on every card row |
Every card carries the receipt for what it cost |
None of this is hackathon garnish. It's the difference between a demo I can leave running and one I take down on Tuesday.
Prize Categories
Best Use of Snowflake.
Ingest, classification, narrative scoring, evidence selection, the prompt, the model call, and the validation of what the model said—all of it happens inside the warehouse, in one procedure. Cloud Run gets structured JSON back and paints a PNG.
There's no ingestion service to point somewhere else, no orchestration layer to rehost, and no application code that knows what a storyline is. The detector, the evidence budget, the prompt, and the thirteen checks guarding the model's answer are all SQL, and they all live in
SOCIAL SHARE CARD GENERATOR