<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: André Ahlert</title>
    <description>The latest articles on Forem by André Ahlert (@andreahlert).</description>
    <link>https://forem.com/andreahlert</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3824067%2Fe332f5fe-20ae-4eb9-9f99-ccb3befeb2ad.png</url>
      <title>Forem: André Ahlert</title>
      <link>https://forem.com/andreahlert</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/andreahlert"/>
    <language>en</language>
    <item>
      <title>The Boring AI Is the Right AI</title>
      <dc:creator>André Ahlert</dc:creator>
      <pubDate>Sun, 17 May 2026 16:00:00 +0000</pubDate>
      <link>https://forem.com/andreahlert/the-boring-ai-is-the-right-ai-55o9</link>
      <guid>https://forem.com/andreahlert/the-boring-ai-is-the-right-ai-55o9</guid>
      <description>&lt;p&gt;At the AI Engineer Summit 2025 in New York, the mantra that got repeated from stage after stage was four words. &lt;em&gt;Capability does not mean reliability.&lt;/em&gt; Speakers from finance, infrastructure, and consumer products converged on the same point: shipping an agent that demos well is now a solved problem, and shipping one that survives a Tuesday in production is not.&lt;/p&gt;

&lt;p&gt;The data backs the room. LangChain's &lt;a href="https://www.langchain.com/state-of-agent-engineering" rel="noopener noreferrer"&gt;State of Agent Engineering&lt;/a&gt; report found that 89 percent of organizations running agents in production have had to add observability that their framework did not give them. Sixty-two percent had to build detailed tracing for individual agent steps. Honeycomb's &lt;a href="https://events.honeycomb.io/o11yConSF2026" rel="noopener noreferrer"&gt;O11yCon 2026&lt;/a&gt; was themed, in full, as the observability conference for the agent era. Three different angles on the same pattern. Teams that took an agent to production had to build half an orchestrator on top of their framework.&lt;/p&gt;

&lt;p&gt;The pattern has a name now. Last month, &lt;a href="https://www.linkedin.com/in/kaxil/" rel="noopener noreferrer"&gt;Kaxil Naik&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/pavan-kumar-gopidesu" rel="noopener noreferrer"&gt;Pavan Kumar Gopidesu&lt;/a&gt; shipped the &lt;a href="https://airflow.apache.org/blog/common-ai-provider/" rel="noopener noreferrer"&gt;Common AI Provider for Apache Airflow 3&lt;/a&gt; with a sentence that articulates what hundreds of teams had been intuiting: &lt;em&gt;"Not a wrapper around another framework, but a provider package that plugs into the orchestrator you already run."&lt;/em&gt; Both work at Astronomer, the commercial backer of Airflow, which is worth naming up front. The sentence is a diagnosis whether it came from Astronomer or anyone else.&lt;/p&gt;

&lt;p&gt;The diagnosis is that the dominant design pattern of the last two years, treating the agent loop as a new runtime, was a category error. The agent loop is not a runtime. It is a workload. The runtime already exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What durable orchestration actually buys you
&lt;/h2&gt;

&lt;p&gt;Three things a mature orchestrator gives an agent that a prototype framework does not.&lt;/p&gt;

&lt;p&gt;The first is durable replay. The Common AI Provider post puts it bluntly: &lt;em&gt;"When a 10-step agent task fails on step 8, a retry shouldn't re-run all 10 steps and double your API bill."&lt;/em&gt; Durable execution caches each model response and each tool result in object storage. A retry serves the cache instead of paying the LLM again. Anyone who has watched an agent loop burn a hundred dollars in a single Sunday night incident will recognize the value of that one line. Frameworks ship retry as a decorator. Orchestrators ship retry as a contract.&lt;/p&gt;

&lt;p&gt;The second is observability that did not have to be invented. Airflow has had structured logging, run history, task duration metrics, and lineage tracking for years, because those features are how a data team trusts a pipeline at all. When the agent becomes a task, the agent inherits everything. There is no instrumentation project. The trace exists because it had to exist for ETL.&lt;/p&gt;

&lt;p&gt;The third is the boring infrastructure that every framework eventually rediscovers. Authentication to three hundred and fifty backends. Role-based access control on who can approve which tool call. Secret management. Connection pooling. Cost attribution by team. None of these are agent features. All of them are required to ship one. Airflow has them because its core customers have been demanding them for a decade. A new framework starts at zero and rebuilds them, badly, in the months that follow its first production incident.&lt;/p&gt;

&lt;p&gt;These three are why the conference circuit converged on reliability as the theme. The talks were not announcing a new problem. They were naming the rebuild bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The category error
&lt;/h2&gt;

&lt;p&gt;Most agent frameworks were designed around the same wrong premise. The premise was that the new thing was the orchestration of LLM calls. If you accept that premise, you build a runtime. You write a scheduler, a retry layer, a state machine, an observability story, a permissions model. You ship the runtime as the framework and the framework owns the lifecycle of the application.&lt;/p&gt;

&lt;p&gt;The premise was off by one. The new thing was the LLM call. Everything around it was already a solved problem. The orchestrator did not need to be invented. It has been in production since 2014. The agent loop is the carry, not the chassis.&lt;/p&gt;

&lt;p&gt;The Airflow team had been building toward this correction for two years before the provider shipped. Airflow 3 reshaped the engine around &lt;a href="https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/assets.html" rel="noopener noreferrer"&gt;assets rather than schedules&lt;/a&gt;, so a pipeline can react to data arriving instead of a clock ticking. The Common AI Provider is the surface layer on top of that foundation, not the diagnosis itself. The diagnosis was the engine work that came first.&lt;/p&gt;

&lt;p&gt;Naik and Gopidesu's line, &lt;em&gt;a provider package that plugs into the orchestrator you already run,&lt;/em&gt; is the cleanest articulation of the correction. It moves the agent from the center of the architecture to the edge. The center stays where it was. The provider model means a team running Airflow gets &lt;code&gt;@task.agent&lt;/code&gt; and &lt;code&gt;@task.llm&lt;/code&gt; as decorators next to the &lt;code&gt;@task&lt;/code&gt; they have been using since Airflow 2.0, and the new code looks like the old code, because it is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Prototype-shape: the agent runtime is the application
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic_ai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai:gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;query_db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read_s3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Analyze churn for Q3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# retry, logging, RBAC, secrets: your problem
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Production-shape: the agent is a task on the orchestrator
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic_ai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai:gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;query_db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read_s3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nd"&gt;@task.agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm_conn_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai_default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;analyze_q3_churn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Analyze churn for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;# retry, logging, RBAC, secrets: the orchestrator's problem
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same agent definition. The difference is where it runs and what it inherits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the framework still wins
&lt;/h2&gt;

&lt;p&gt;Frameworks are not wrong. They are wrong in production. In every other phase of the work, they are correct.&lt;/p&gt;

&lt;p&gt;Prototyping is faster in LangGraph or Pydantic AI than it will ever be in Airflow. The mental model is closer to the code, the iteration loop is shorter, the dependencies are lighter. Sketching a new agent shape when you do not yet know what tools it needs, the right tool is a notebook with a framework, not a DAG.&lt;/p&gt;

&lt;p&gt;Exploratory work belongs there too. Research, evaluation harnesses, small internal tools one person runs once a week. None of these justify the operational weight of an orchestrator. None of them suffer from missing durable replay because they do not run unattended.&lt;/p&gt;

&lt;p&gt;The framework wins everywhere the agent is not yet load-bearing. The provider wins the moment the agent has to survive a holiday weekend without you watching it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-line decision
&lt;/h2&gt;

&lt;p&gt;Here is the heuristic, two lines.&lt;/p&gt;

&lt;p&gt;If the agent is not yet running unattended on a schedule and not yet paid for by a customer, keep it in the framework. If it is either of those, move it behind a provider on an orchestrator you already run.&lt;/p&gt;

&lt;p&gt;That is the line. It is not a commitment to Airflow specifically. The same logic applies if your orchestrator is Dagster, Prefect, or Temporal. The principle is that durable execution is not a checkbox on a roadmap. It is a contract between the engine and the workload, and prototype frameworks ship engines that do not honor that contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the pattern says about software
&lt;/h2&gt;

&lt;p&gt;The pattern is not new. Rails won the web because it absorbed the request-response cycle until that cycle became invisible. Kubernetes won infrastructure because it absorbed the deploy-and-restart loop until the loop became invisible. Postgres absorbed twenty years of small databases because each of those small databases eventually rediscovered transactions, recovery, and indexing, badly. Every time the boring layer wins, it wins because newcomers underestimate how much the boring layer was already doing.&lt;/p&gt;

&lt;p&gt;Production AI is in that moment. The boring layer is the orchestrator. The newcomer is the agent framework. The newcomer is not going away, because the newcomer is correct in the half of the lifecycle where the boring layer is too heavy. The boring layer is not going away either, because the moment the agent goes load-bearing, the rebuild begins, and the rebuild is the orchestrator. The signal that this has moved from contrarian read to industry consensus is the framing of &lt;a href="https://www.astronomer.io/blog/state-of-airflow-2026/" rel="noopener noreferrer"&gt;Astronomer's State of Airflow 2026&lt;/a&gt; report itself: "The Orchestration Layer is Uniting Data, AI, and Enterprise Growth." Two years ago the orchestrator was a deployment concern. In 2026 it is the consolidating layer.&lt;/p&gt;

&lt;p&gt;Naming the pattern early is the move. Teams who name it spend their second quarter shipping product. Teams who do not spend it rebuilding retry logic and calling it agent engineering.&lt;/p&gt;

&lt;p&gt;The boring AI is the right AI. Borrowed term, durable claim.&lt;/p&gt;




&lt;p&gt;I am building &lt;a href="https://kilnx.org" rel="noopener noreferrer"&gt;Kilnx&lt;/a&gt;, a declarative backend DSL that pairs with htmx, and &lt;a href="https://provero.org" rel="noopener noreferrer"&gt;Provero&lt;/a&gt;, where a lot of the orchestration-shape decisions I write about are the day job. If the diagnosis here lands, that is the door.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;André Ahlert is a product engineer. Contributor across Apache, Flyte, Backstage, HTMX, Hyperscript. Currently building &lt;a href="https://kilnx.org" rel="noopener noreferrer"&gt;Kilnx&lt;/a&gt; and &lt;a href="https://provero.org" rel="noopener noreferrer"&gt;Provero&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>airflow</category>
      <category>python</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Why I Built a Language Instead of a Framework</title>
      <dc:creator>André Ahlert</dc:creator>
      <pubDate>Sat, 16 May 2026 21:08:36 +0000</pubDate>
      <link>https://forem.com/andreahlert/why-i-built-a-language-instead-of-a-framework-5g65</link>
      <guid>https://forem.com/andreahlert/why-i-built-a-language-instead-of-a-framework-5g65</guid>
      <description>&lt;p&gt;A friend asked me, around the third week of working on &lt;a href="https://kilnx.org" rel="noopener noreferrer"&gt;Kilnx&lt;/a&gt;, why I had not just written it as an Express plugin. The question was fair. It was also the same question my own brain had been asking me for a month.&lt;/p&gt;

&lt;p&gt;The honest answer took me longer to find than I would like to admit. This piece is what I would have said to him in November if I had already understood what I was doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The push
&lt;/h2&gt;

&lt;p&gt;I had been shipping backends. Some of them were small. Most of them were not. A multi-tenant CRM where every query had to filter by &lt;code&gt;org_id&lt;/code&gt; or you leaked data across customers. An internal admin tool with role-based pages, scheduled jobs, outbound webhooks signed with HMAC. A SaaS dashboard with background workers, rate-limited APIs, an LLM call inside a critical path. None of these are blogs. All of them carried the same shape.&lt;/p&gt;

&lt;p&gt;The interesting work in each project was the domain. The uninteresting work was identical. Auth setup. Session management. CSRF wiring. Connection pool tuning. Migration script naming. Multi-tenant guards on every query. Webhook signature verification. The right way to call Claude from a background job without burning the bill on retry. By the time I had a first feature shipped, I had touched a dozen files and made forty decisions, none of which had anything to do with what the customer wanted.&lt;/p&gt;

&lt;p&gt;I started counting. Two thirds of the lines in those projects were about plumbing. The other third was the product. The numbers held across three projects. The numbers held across two stacks. The plumbing was not a project artifact. It was the toolchain's signature, and it was showing up in every project I touched.&lt;/p&gt;

&lt;p&gt;That is what pushed me toward a language. Not a feeling. A pattern that did not move when I changed teams, customers, or stacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constitution
&lt;/h2&gt;

&lt;p&gt;The repo has a file called &lt;a href="https://github.com/kilnx-org/kilnx/blob/main/PRINCIPLES.md" rel="noopener noreferrer"&gt;PRINCIPLES.md&lt;/a&gt;. The first principle, numbered zero because it predates the others, reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The complexity is the tool's fault, not the problem's. Most web apps are not complex. They are lists, forms, dashboards, CRUDs. The complexity comes from the tools we use, not from the problem we are solving. Kilnx exists to prove this.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is a claim. The rest of the language is the test of the claim. If you accept the premise, the design follows. If you reject the premise, nothing about Kilnx makes sense. The interesting argument is whether the premise is true, not whether the design is clever.&lt;/p&gt;

&lt;p&gt;I think it is mostly true. Not entirely. Some web work is genuinely complex and would be complex in any tool. But the line between "complex problem" and "complex tool" runs further toward the tool side than most engineers want to admit, and the way to find out which side a given complexity sits on is to build a tool that subtracts itself and see what remains.&lt;/p&gt;

&lt;p&gt;Here is what a working slice of the language looks like. Authenticated task list, htmx delete, paginated query, all in one file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model task
  title: text required
  done: bool default false
  created: timestamp auto

auth
  table: user
  identity: email
  password: password
  login: /login
  after login: /tasks

page /tasks requires auth
  query tasks: SELECT id, title, done FROM task
               WHERE owner = :current_user.id
               ORDER BY created DESC paginate 20
  html
    {{each tasks}}
      &amp;lt;tr&amp;gt;
        &amp;lt;td&amp;gt;{title}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;{{if done}}Yes{{end}}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;&amp;lt;button hx-post="/tasks/{id}/delete"
                    hx-target="closest tr"
                    hx-swap="outerHTML"&amp;gt;Delete&amp;lt;/button&amp;gt;&amp;lt;/td&amp;gt;
      &amp;lt;/tr&amp;gt;
    {{end}}

action /tasks/:id/delete requires auth
  query: DELETE FROM task WHERE id = :id AND owner = :current_user.id
  respond fragment delete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That file is the whole app. Registration, login with bcrypt, sessions, CSRF on the htmx POST, parameter binding on every query, pagination, ownership check on delete. The Express equivalent is between four hundred and six hundred lines across eight files, depending on which middleware you copy versus extract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious objection
&lt;/h2&gt;

&lt;p&gt;Build a framework, not a language. Rails exists. Phoenix exists. Django exists. Whatever you think is broken about backend work, somebody already wrapped your favorite host language in a thinner thing and called it a framework. Pick one and contribute.&lt;/p&gt;

&lt;p&gt;That was the version of the argument I kept hearing, including from myself. It did not land for one structural reason. Frameworks always lose the constraint fight, and the constraint fight is exactly the fight Kilnx is trying to win.&lt;/p&gt;

&lt;p&gt;A framework lives inside the host language. The host language gives you escape hatches at every level. You want to bypass the router? Reach for the HTTP server. You want to skip the ORM? Drop into raw SQL. You want to ignore the tenant guard? Comment it out, the language will let you. Every framework I have shipped real work on, by month six, has half its codebase in the official patterns and the other half in escape hatches. The escape hatches are not bugs. They are the price the framework pays to live as a tenant inside a general-purpose language.&lt;/p&gt;

&lt;p&gt;I did not want a tenant. I wanted a contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a language can refuse
&lt;/h2&gt;

&lt;p&gt;Five things turned out to be impossible inside a framework that became natural inside a language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compile-time SQL safety.&lt;/strong&gt; In any framework, your SQL lives in strings, or in an ORM that compiles strings, or in a query builder that pretends not to. The framework cannot validate your queries at compile time because the host language cannot see them at compile time. Kilnx queries are parsed by the same compiler that parses the rest of the program. A column rename in a model fails to compile every query that referenced it. SQL injection is not blocked by an escape function. It is blocked by the grammar refusing to interpolate untyped strings into SQL position.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenant guards as syntax.&lt;/strong&gt; Every SaaS backend I have shipped had the same bug class. Someone forgot to add &lt;code&gt;WHERE org_id = :current_user.org_id&lt;/code&gt; to a query, and one tenant could read another tenant's data. The bug class exists because the host language sees the missing filter as legal code. In Kilnx, a &lt;code&gt;tenant&lt;/code&gt; modifier on the model produces a fail-closed guard that the analyzer enforces on every query path. If you write a query that does not scope to the tenant, the compiler refuses to build the binary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model invoice tenant
  amount: int required
  customer: text required

page /invoices requires auth
  query invoices: SELECT id, amount, customer FROM invoice
  html
    {{each invoices}}&amp;lt;p&amp;gt;{customer}: {amount}&amp;lt;/p&amp;gt;{{end}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kilnx check app.kilnx
error: query in page /invoices is missing tenant scope
  app.kilnx:5: query invoices: SELECT id, amount, customer FROM invoice
  hint: tenant model 'invoice' requires WHERE org_id = :current_user.org_id
  hint: or use `unscoped: explicit-reason` to opt out for a specific query
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A framework can warn you. A language can refuse to build. The pattern is documented in the &lt;a href="https://github.com/kilnx-org/kilnx/pull/52" rel="noopener noreferrer"&gt;tenant rollout PR&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM agents as first-class language constructs.&lt;/strong&gt; The piece I wrote last week argued that agents in production end up as tasks on the orchestrator you already run. The same logic applies one layer down. An agent inside a request handler is a task on the language you already run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mcp linear
  command: linear-mcp-server
  env: LINEAR_API_KEY=:env.LINEAR_API_KEY

action /tickets/:id/triage
  agent classify
    prompt: "Classify ticket {ticket.body} into one of: bug, feature, support."
    permission-mode: plan
    max-budget-usd: 0.25
    max-turns: 3
    mcp: linear
  query: UPDATE ticket SET category = :classify.text, cost_usd = :classify.cost_usd
         WHERE id = :id
  respond fragment ticket-row
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;agent&lt;/code&gt; spawns a Claude CLI subprocess. &lt;code&gt;:classify.text&lt;/code&gt;, &lt;code&gt;:classify.session_id&lt;/code&gt;, &lt;code&gt;:classify.cost_usd&lt;/code&gt;, &lt;code&gt;:classify.stop_reason&lt;/code&gt; are bound for the rest of the action. &lt;code&gt;max-budget-usd&lt;/code&gt; is enforced by the runtime. &lt;code&gt;mcp: linear&lt;/code&gt; mounts the MCP server declared at the top of the file. The frame around the agent is grammar, not glue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrations as a controlled surface.&lt;/strong&gt; &lt;code&gt;kilnx migrate&lt;/code&gt; detects drift across five dimensions: orphan columns, type mismatch, NOT NULL mismatch, single-column UNIQUE mismatch, DEFAULT presence mismatch. Migrations themselves are additive, never destructive. The language took a position on what is safe to do automatically and what requires the human to look.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kilnx migrate app.kilnx
applying schema...
warning: orphan column
  invoice.legacy_status (DB has it, model does not declare it)
  hint: drop manually after data migration
warning: type mismatch
  user.id (DB: integer, model: uuid)
  hint: requires data migration plus ALTER, not auto-generated
warning: NOT NULL mismatch
  task.due_date (DB: nullable, model: required)
  hint: backfill defaults before tightening
warning: UNIQUE mismatch
  account.slug (DB: not unique, model: unique)
  hint: dedupe rows before adding the constraint
warning: DEFAULT presence mismatch
  task.done (DB: no default, model: default false)
  hint: review before relying on the default in new code
migration applied with 5 warnings.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A framework can ship a migration tool. A language can make the migration tool part of the same compile pass that builds your routes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-binary deploy.&lt;/strong&gt; A framework runs on top of a runtime that you also have to deploy. Node. Python. Ruby. Each brings a package manager, a lockfile, a Dockerfile, a &lt;code&gt;node_modules&lt;/code&gt; directory the size of a small operating system. Kilnx compiles a &lt;code&gt;.kilnx&lt;/code&gt; file to a fifteen-megabyte binary that embeds the HTTP server, the database driver, the htmx JavaScript, and your application. &lt;code&gt;scp&lt;/code&gt; it to a server and run it. The deploy story is &lt;code&gt;./myapp&lt;/code&gt;. A framework can shrink the deploy story. A language can collapse it.&lt;/p&gt;

&lt;p&gt;Notice the pattern. A framework can make these things easier. A language can make their alternatives impossible. The asymmetry is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Building a language is more work than building a framework. This is the easy half of the trade-off to name. The repo is nineteen thousand lines of Go and three hundred eleven tests with race detection, to deliver something whose feature list on paper looks like a slightly opinionated web framework. If a small web framework was what I wanted, building the framework would have been the right answer.&lt;/p&gt;

&lt;p&gt;The harder half is that a language has to take itself seriously. The grammar has to be coherent. The error messages have to be useful. The tooling has to exist. There is no falling back on someone else's ecosystem when something is missing. You either ship the LSP server or your users do not get autocomplete. You either ship the test runner or your users do not get tests. You either ship the playground or your users cannot evaluate the language without installing it. You either auto-generate an &lt;a href="https://github.com/kilnx-org/kilnx/blob/main/AGENTS.md" rel="noopener noreferrer"&gt;AGENTS.md&lt;/a&gt; for coding agents or your users get LLMs inventing keywords that do not exist.&lt;/p&gt;

&lt;p&gt;The third cost is that a language refuses things, and refusing things is socially expensive. Every refusal is a fight with somebody who has a perfectly reasonable use case that the language does not serve. Frameworks can absorb those use cases with an escape hatch. Languages cannot. You have to look someone in the eye and tell them the language is not going to do that, and that the reason is that doing it would break the contract.&lt;/p&gt;

&lt;p&gt;A friend told me that the part of building a language nobody warns you about is that you have to say no to a lot of people who are right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it gives back
&lt;/h2&gt;

&lt;p&gt;The give-back is the part that justifies the cost. It is also the part that does not fit on a marketing page, because it has to be measured rather than read about. So measure.&lt;/p&gt;

&lt;p&gt;The blog example in the repo is ninety-four lines in a single file. The Express equivalent is between four hundred and six hundred lines across eight files, depending on which middleware you copy versus extract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Kilnx blog            Express + Prisma + EJS blog
──────────────────    ──────────────────────────────
app.kilnx        94   app.js                     62
                      routes/auth.js             88
                      routes/posts.js           104
                      models/Post.js             36
                      middleware/csrf.js         24
                      middleware/session.js      31
                      db/migrations/             47
                      views/                     94
                      ──────────                 ───
                      8 files                   486
1 file           94                            ~480
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Kilnx version is short because the language absorbed the rest, not because the app does less. The same things ship in both columns. The difference is which side wrote them.&lt;/p&gt;

&lt;p&gt;What disappears on the Kilnx side, never written by the user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bcrypt password hashing       auto from `auth`
session cookies, signed       auto from `auth`
CSRF on every POST/PUT/DELETE auto on every action
SQL parameter binding         only form the grammar allows
HTML escaping in templates    only form the grammar allows
multi-tenant scoping          refused at compile time if missing
schema migrations             same compile pass that builds routes
LLM agent budget enforcement  required attribute on `agent` blocks
MCP server lifecycle          managed by the runtime
HTTP server, routing, logs    embedded in the binary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other give-back is harder to measure but bigger. The constraints stop you from drifting. There is no point at which you can decide to do auth a different way and have it cost you nothing. The decision was made when the language was designed. You inherit it. The cognitive load of every project drops because the design space is smaller.&lt;/p&gt;

&lt;p&gt;For most product work, smaller design space is the gift you have been begging the universe for.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a framework is the right answer
&lt;/h2&gt;

&lt;p&gt;The inverse is real and worth naming.&lt;/p&gt;

&lt;p&gt;If your work needs escape hatches more often than it needs constraints, a framework is the right shape. Custom integrations against an irregular set of third-party systems, custom protocols, custom transport, custom auth flows that do not fit any standard pattern. Days that are ninety percent edge cases. A framework lets you write the edge case directly in the host language without the language fighting you.&lt;/p&gt;

&lt;p&gt;Convex made the same trade in the agent world. They accepted the determinism contract, and they paid the cost of not being able to do arbitrary side effects in mutations. For most product workloads that cost is fine. For some, it is too high. The same logic applies here. Kilnx accepts a constraint contract, and the contract is wrong for some workloads. The question is whether your workload is one of them, and the honest answer is usually no.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the bet really is
&lt;/h2&gt;

&lt;p&gt;The bet at the center of Kilnx is that a specific opinion, taken seriously, eats a category of work that nobody wanted to be doing anyway. Pick the right opinion, encode it past the point where users can opt out, and the opinion becomes leverage. The leverage shows up as code that did not need to be written. Two thirds of every project, in my experience.&lt;/p&gt;

&lt;p&gt;A framework can host an opinion. A language can enforce one. The reason I built a language is that I wanted the enforcement, and I had counted the lines of plumbing in enough projects to know what the enforcement was worth.&lt;/p&gt;

&lt;p&gt;Kilnx is in early release. The grammar is twenty-seven keywords. The compiler is a few thousand commits old. None of that matters as much as the bet does, and the bet is what is being tested in production over the next year.&lt;/p&gt;

&lt;p&gt;The spreadsheet was real. The language was the honest answer to it.&lt;/p&gt;




&lt;p&gt;I am building &lt;a href="https://kilnx.org" rel="noopener noreferrer"&gt;Kilnx&lt;/a&gt;, a declarative backend language that pairs with htmx, and &lt;a href="https://provero.org" rel="noopener noreferrer"&gt;Provero&lt;/a&gt;, where a lot of the language-shape decisions I write about are the day job. If the diagnosis here lands, that is the door.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;André Ahlert is a product engineer. Contributor across Apache, Flyte, Backstage, HTMX, Hyperscript. Currently building &lt;a href="https://kilnx.org" rel="noopener noreferrer"&gt;Kilnx&lt;/a&gt; and &lt;a href="https://provero.org" rel="noopener noreferrer"&gt;Provero&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>go</category>
      <category>htmx</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Soda Moved to ELv2. Provero Is Apache 2.0.</title>
      <dc:creator>André Ahlert</dc:creator>
      <pubDate>Wed, 01 Apr 2026 14:19:06 +0000</pubDate>
      <link>https://forem.com/andreahlert/soda-moved-to-elv2-provero-is-apache-20-42l9</link>
      <guid>https://forem.com/andreahlert/soda-moved-to-elv2-provero-is-apache-20-42l9</guid>
      <description>&lt;p&gt;When Soda changed its license from Apache 2.0 to Elastic License v2, teams that relied on Soda Core as open source infrastructure had to re-evaluate. This post explains what changed, what it means for you, and what alternatives exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;Soda Core was originally released under Apache License 2.0. In 2023, Soda switched to the Elastic License v2 (ELv2). The change applied to all new versions of Soda Core and its associated packages.&lt;/p&gt;

&lt;p&gt;ELv2 is not an open source license by the OSI definition. It adds two restrictions that Apache 2.0 does not have: you cannot offer the software as a managed service, and you cannot modify the license key functionality. For internal use at most companies, ELv2 is permissive enough. But for platform vendors, consultancies embedding Soda in their products, or organizations with strict open source policies, it creates friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is affected
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Internal data teams&lt;/strong&gt; (Low impact) -- ELv2 allows internal use. You can keep using Soda Core if the license terms work for your legal team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data platform vendors&lt;/strong&gt; (High impact) -- If you embed data quality checks in a product you sell, ELv2 prohibits offering it as a managed service without a commercial agreement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consultancies and integrators&lt;/strong&gt; (Medium impact) -- Depends on how you distribute. If you ship Soda as part of a client deployment, review the license terms with legal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open source projects&lt;/strong&gt; (High impact) -- ELv2 is not OSI-approved. If your project requires OSI-approved dependencies, you cannot depend on Soda Core.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is a pattern, not an exception
&lt;/h2&gt;

&lt;p&gt;Soda is not the first data tool to make this move. The playbook is familiar across the industry: release as open source, build adoption, then change the license to protect a commercial offering. Elastic did it. MongoDB did it. HashiCorp did it. Each time, the community had to decide whether to accept the new terms, fork the project, or find an alternative.&lt;/p&gt;

&lt;p&gt;The pattern is rational from a business perspective. But it breaks trust with teams who built infrastructure on the assumption that the license would not change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Provero does differently
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/provero-org/provero" rel="noopener noreferrer"&gt;Provero&lt;/a&gt; is licensed under Apache 2.0. Every feature ships in the open source package: anomaly detection, data contracts, all 16 check types, the CLI, the Airflow provider. There is no cloud-only tier and no feature gating.&lt;/p&gt;

&lt;p&gt;We are pursuing acceptance into the LF AI &amp;amp; Data Foundation, which means the project would be governed by a neutral foundation, not a single company. Foundation governance makes unilateral license changes structurally difficult.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Provero&lt;/th&gt;
&lt;th&gt;Soda Core&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;ELv2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSI approved&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed service allowed&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anomaly detection&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Cloud only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data contracts&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Partial (Cloud for full)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;Targeting LF AI Foundation&lt;/td&gt;
&lt;td&gt;Soda Inc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check format&lt;/td&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;td&gt;SodaCL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;provero import soda&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Migrating from Soda
&lt;/h2&gt;

&lt;p&gt;If you have existing SodaCL checks, Provero includes a converter that maps Soda check syntax to Provero YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;provero
provero import soda checks.yaml &lt;span class="nt"&gt;-o&lt;/span&gt; provero.yaml
provero run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SodaCL&lt;/th&gt;
&lt;th&gt;Provero&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;missing_count(col) = 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;not_null: col&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;duplicate_count(col) = 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unique: col&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;row_count &amp;gt; 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;row_count: { min: 1 }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;freshness(col) &amp;lt; 24h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;freshness: { column: col, max_age: 24h }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;valid_count(col) = ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;accepted_values: { column: col, values: [...] }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Checks that don't have a direct equivalent are preserved as YAML comments, so nothing is silently dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our position on licensing
&lt;/h2&gt;

&lt;p&gt;We think data quality is infrastructure. It belongs in the same category as linters, test frameworks, and CI tools. You would not accept a linter that moved half its rules behind a paywall. Data quality checks should work the same way: open, portable, composable.&lt;/p&gt;

&lt;p&gt;Provero will stay Apache 2.0. Not because we are against commercial models, but because we believe the right way to build a business around open source is to sell services, hosting, and support on top of a fully open core. Not to restrict the core itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;provero
provero init
provero run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/provero-org/provero" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://pypi.org/project/provero/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt; | &lt;a href="https://provero-org.github.io/provero/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>dataengineering</category>
      <category>devops</category>
    </item>
    <item>
      <title>I built a backend language that a 3B model writes better than Express</title>
      <dc:creator>André Ahlert</dc:creator>
      <pubDate>Wed, 01 Apr 2026 10:40:12 +0000</pubDate>
      <link>https://forem.com/andreahlert/i-built-a-backend-language-that-a-3b-model-writes-better-than-express-2im3</link>
      <guid>https://forem.com/andreahlert/i-built-a-backend-language-that-a-3b-model-writes-better-than-express-2im3</guid>
      <description>&lt;p&gt;I've been building web apps for years and the thing that always bothered me is how much ceremony goes into something that should be simple. A task list with auth shouldn't need 15 files across 3 directories with 200 lines of config.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/kilnx-org/kilnx" rel="noopener noreferrer"&gt;Kilnx&lt;/a&gt;, a declarative backend language. 27 keywords, compiles to a single binary, SQL inline, HTML as output. At some point I started wondering: if the language is this small, can a tiny local LLM write it? A model that fits on a phone?&lt;/p&gt;

&lt;p&gt;I ran the benchmark. Kilnx won every round.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Kilnx looks like
&lt;/h2&gt;

&lt;p&gt;A complete app with auth, pagination, htmx, and a SQLite database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config
  database: "sqlite://app.db"
  port: 8080
  secret: env SECRET_KEY required

model user
  name: text required
  email: email unique
  password: password required

model task
  title: text required
  done: bool default false
  owner: user required
  created: timestamp auto

auth
  table: user
  identity: email
  password: password
  login: /login
  after login: /tasks

page /tasks requires auth
  query tasks: SELECT id, title, done FROM task
               WHERE owner = :current_user.id
               ORDER BY created DESC paginate 20
  html
    {{each tasks}}
    &amp;lt;tr&amp;gt;
      &amp;lt;td&amp;gt;{title}&amp;lt;/td&amp;gt;
      &amp;lt;td&amp;gt;{{if done}}Yes{{end}}&amp;lt;/td&amp;gt;
      &amp;lt;td&amp;gt;
        &amp;lt;button hx-post="/tasks/{id}/delete"
                hx-target="closest tr"
                hx-swap="outerHTML"&amp;gt;Delete&amp;lt;/button&amp;gt;
      &amp;lt;/td&amp;gt;
    &amp;lt;/tr&amp;gt;
    {{end}}

action /tasks/create method POST requires auth
  validate task
  query: INSERT INTO task (title, owner)
         VALUES (:title, :current_user.id)
  redirect /tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kilnx build app.kilnx -o myapp&lt;/code&gt; gives you a ~15MB binary. Registration, login with bcrypt, sessions, CSRF, validation, pagination, htmx inline delete. No framework, no ORM, no node_modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;The Kilnx grammar fits in 400 lines of docs. Express, Django, and Node.js each have thousands of pages of documentation, dozens of APIs, and multiple ways to do the same thing.&lt;/p&gt;

&lt;p&gt;I wanted to know if that difference in surface area shows up when you ask small LLMs to generate code. Not GPT-4 or Claude, but models you run on a laptop with Ollama. Models between 1B and 7B parameters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;I wrote 10 equivalent tasks across four stacks (Kilnx, Express, Django, vanilla Node.js):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Difficulty&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Hello World page&lt;/td&gt;
&lt;td&gt;trivial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;User model definition&lt;/td&gt;
&lt;td&gt;easy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Page with database query&lt;/td&gt;
&lt;td&gt;easy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Create with validation&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Auth + protected route&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Delete with htmx response&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;SSE notifications&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Chat websocket&lt;/td&gt;
&lt;td&gt;hard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Stripe webhook&lt;/td&gt;
&lt;td&gt;hard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Complete mini app&lt;/td&gt;
&lt;td&gt;hard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Five models, three families, all local:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;Disk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 2.5 7B&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;4.7 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 2.5 3B&lt;/td&gt;
&lt;td&gt;3B&lt;/td&gt;
&lt;td&gt;1.9 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 2.5 1.5B&lt;/td&gt;
&lt;td&gt;1.5B&lt;/td&gt;
&lt;td&gt;986 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phi-4 Mini&lt;/td&gt;
&lt;td&gt;3.8B&lt;/td&gt;
&lt;td&gt;2.5 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.2 1B&lt;/td&gt;
&lt;td&gt;1B&lt;/td&gt;
&lt;td&gt;1.3 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three validation passes on every output:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keyword matching&lt;/strong&gt; - does the code contain the structural elements the task requires?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Syntax check&lt;/strong&gt; - &lt;code&gt;kilnx check&lt;/code&gt; (semantic analysis), &lt;code&gt;node --check&lt;/code&gt;, &lt;code&gt;python compile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM-as-judge&lt;/strong&gt; - Qwen 7B rating syntax/completeness/correctness/idiom (0-3 each)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every combination ran 3 times. 600 generations, 600 judge evaluations.&lt;/p&gt;

&lt;h3&gt;
  
  
  About fairness
&lt;/h3&gt;

&lt;p&gt;This is important. &lt;strong&gt;Kilnx has never appeared in any training dataset.&lt;/strong&gt; Zero &lt;code&gt;.kilnx&lt;/code&gt; files exist on the internet outside my repo. Express and Django have millions of code examples baked into every LLM's weights.&lt;/p&gt;

&lt;p&gt;I gave the models the Kilnx grammar reference (11.7K chars) as prompt context. Express, Django, and Node got no reference docs because they don't need them.&lt;/p&gt;

&lt;p&gt;If anything, this setup gives the established frameworks a huge advantage. They've been pre-trained on the entire Stack Overflow + GitHub history. Kilnx gets one document.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Structural correctness (keyword score, averaged over 3 runs)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Kilnx&lt;/th&gt;
&lt;th&gt;Express&lt;/th&gt;
&lt;th&gt;Node.js&lt;/th&gt;
&lt;th&gt;Django&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 2.5 7B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;93%&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen 2.5 3B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;89%&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 2.5 1.5B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phi-4 Mini&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;98%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;93%&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.2 1B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;90%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;td&gt;77%&lt;/td&gt;
&lt;td&gt;77%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Qwen 3B, a 1.9 GB model, scores 99% on Kilnx, a language it has never encountered. The same model gets 87% on Django, a framework it has seen millions of times during training.&lt;/p&gt;

&lt;p&gt;When you shrink from 7B down to 1B, Kilnx drops 10 points. Node.js drops 16. The simpler grammar holds up better as the model gets dumber.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tokens per task (completion only)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Qwen 7B&lt;/th&gt;
&lt;th&gt;Qwen 3B&lt;/th&gt;
&lt;th&gt;Qwen 1.5B&lt;/th&gt;
&lt;th&gt;Phi-4 Mini&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kilnx&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;105&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;112&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;111&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;95&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Django&lt;/td&gt;
&lt;td&gt;195&lt;/td&gt;
&lt;td&gt;226&lt;/td&gt;
&lt;td&gt;152&lt;/td&gt;
&lt;td&gt;199&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Express&lt;/td&gt;
&lt;td&gt;302&lt;/td&gt;
&lt;td&gt;349&lt;/td&gt;
&lt;td&gt;265&lt;/td&gt;
&lt;td&gt;315&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;347&lt;/td&gt;
&lt;td&gt;381&lt;/td&gt;
&lt;td&gt;333&lt;/td&gt;
&lt;td&gt;490&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;3x fewer tokens than Express/Node. This is not a style difference. It's the same functionality. A chat websocket in Kilnx is ~110 tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;socket /chat/:room requires auth
  on connect
    query: select body, author.name, created from chat_message
           where room = :room
           order by created desc
           limit 50
    send history

  on message
    validate
      body: required max 500
    query: insert into chat_message (body, author, room)
           values (:body, :current_user.id, :room)
    broadcast to :room fragment chat-bubble
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Express version of the same task runs ~420 tokens of socket.io setup, middleware, database calls, and room management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session economics
&lt;/h3&gt;

&lt;p&gt;Kilnx has a cost that Express doesn't: the grammar reference takes ~3,100 prompt tokens. But that's loaded once per session. The per-task completion cost is what scales.&lt;/p&gt;

&lt;p&gt;Over a real session with Qwen 3B:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tasks&lt;/th&gt;
&lt;th&gt;Kilnx&lt;/th&gt;
&lt;th&gt;Express&lt;/th&gt;
&lt;th&gt;Node.js&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3,269&lt;/td&gt;
&lt;td&gt;464&lt;/td&gt;
&lt;td&gt;501&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,277&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,640&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5,010&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5,957&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11,600&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12,525&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8,757&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23,200&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25,050&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14,357&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;46,400&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50,100&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Kilnx becomes cheaper than Express at task 9. By the end of a workday (call it 50-100 tasks with a copilot), you've used 71% fewer tokens. If you're paying per token on an API, that's real money. If you're running locally, it's real time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Raw output from the 3B
&lt;/h3&gt;

&lt;p&gt;No editing, no cherry-picking. This is Qwen 2.5 3B (1.9 GB on disk) generating a complete app from scratch, having seen the Kilnx grammar for the first time in the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config
  database: env DATABASE_URL default "sqlite://app.db"
  port: 8080
  secret: env SECRET_KEY required

model task
  title: text required
  done: bool default false
  owner: user required
  created: timestamp auto

auth
  table: user
  identity: email
  password: password
  login: /login
  after login: /tasks

page /tasks layout main requires auth
  query tasks: select id, title, done from task
               where owner = :current_user.id
               order by created desc
               paginate 20
  html
    &amp;lt;input type="search" name="q" placeholder="Search tasks..."
           hx-get="/tasks" hx-trigger="keyup changed delay:300ms"
           hx-target="#task-list"&amp;gt;
    &amp;lt;table id="task-list"&amp;gt;
      &amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Title&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Done&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;
      {{each tasks}}
      &amp;lt;tr&amp;gt;
        &amp;lt;td&amp;gt;{title}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;{{if done}}Yes{{end}}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;&amp;lt;button hx-post="/tasks/{id}/delete"
                    hx-target="closest tr"
                    hx-swap="outerHTML"&amp;gt;Delete&amp;lt;/button&amp;gt;&amp;lt;/td&amp;gt;
      &amp;lt;/tr&amp;gt;
      {{end}}
    &amp;lt;/table&amp;gt;

action /tasks/create method POST requires auth
  validate task
  query: insert into task (title, owner)
         values (:title, :current_user.id)
  redirect /tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auth, pagination, htmx search with debounce, inline delete, form validation. It even added the search input on its own, that wasn't in the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I think this happens
&lt;/h2&gt;

&lt;p&gt;Express forces the model to make a lot of decisions. CommonJS or ESM? Which middleware in what order? Prisma or Sequelize or raw queries? Passport or express-session or JWT? EJS or Pug or Handlebars? Each fork is a place where a small model can pick wrong.&lt;/p&gt;

&lt;p&gt;Kilnx has one way to do each thing. One keyword for auth, one keyword for pages, one for actions. The model doesn't pick between approaches because there's only one approach. The decision space is so small that even a 1B model mostly gets it right.&lt;/p&gt;

&lt;p&gt;I don't think this is unique to Kilnx. Any DSL with a tight, regular grammar would probably show the same pattern. &lt;strong&gt;The surface area of a language directly predicts how well small models can generate it.&lt;/strong&gt; I haven't seen anyone optimize for that yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do with this
&lt;/h2&gt;

&lt;p&gt;If you're an indie dev or a solo founder shipping CRUD apps:&lt;/p&gt;

&lt;p&gt;A 3B model running locally gives you 99% accuracy on Kilnx with no API costs, no internet, no privacy concerns. The 7B hits 100%. You don't need to send your code to OpenAI to get a working backend.&lt;/p&gt;

&lt;p&gt;If you're using a paid API, the 71% token reduction over a session adds up fast. Especially if you're iterating on features all day.&lt;/p&gt;

&lt;p&gt;If you're just curious, the whole language is 27 keywords. You can read &lt;a href="https://github.com/kilnx-org/kilnx/blob/main/GRAMMAR.md" rel="noopener noreferrer"&gt;the grammar&lt;/a&gt; in 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/kilnx-org/kilnx/main/install.sh | sh
kilnx run app.kilnx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/kilnx-org/kilnx" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kilnx-org/kilnx/blob/main/GRAMMAR.md" rel="noopener noreferrer"&gt;Grammar reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kilnx-org/kilnx-example-chat" rel="noopener noreferrer"&gt;Slack Alternative example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kilnx-org/kilnx-org/tree/main/paper" rel="noopener noreferrer"&gt;Benchmark scripts and raw data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I built an MCP Server that lets Claude manage your Substack</title>
      <dc:creator>André Ahlert</dc:creator>
      <pubDate>Sat, 14 Mar 2026 14:07:52 +0000</pubDate>
      <link>https://forem.com/andreahlert/i-built-an-mcp-server-that-lets-claude-manage-your-substack-1eb2</link>
      <guid>https://forem.com/andreahlert/i-built-an-mcp-server-that-lets-claude-manage-your-substack-1eb2</guid>
      <description>&lt;p&gt;The Substack web UI is fine for casual use, but if you're a power user who publishes daily, manages engagement, and wants to automate interactions, you need something faster.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jis4mml3w6kj9a5sxrb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jis4mml3w6kj9a5sxrb.gif" alt="TUI Gif Interface Demo" width="705" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built &lt;code&gt;@postcli/substack&lt;/code&gt;, a tool that gives you three interfaces to Substack:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. CLI&lt;/strong&gt; - Direct commands from your terminal&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;postcli-substack notes publish &lt;span class="s2"&gt;"Shipping fast from the terminal"&lt;/span&gt;
postcli-substack posts list &lt;span class="nt"&gt;--limit&lt;/span&gt; 5
postcli-substack feed &lt;span class="nt"&gt;--tab&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="nt"&gt;-you&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. TUI&lt;/strong&gt; - Full interactive terminal UI with 6 tabs&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;postcli-substack tui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate with j/k or arrow keys, scroll with mouse wheel, open posts in browser with 'o'. It's keyboard-driven and fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. MCP Server&lt;/strong&gt; - 16 tools for AI agents&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"substack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postcli-substack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tell Claude "like back everyone who liked my last note" and it just works.&lt;/p&gt;

&lt;h3&gt;
  
  
  The automation engine
&lt;/h3&gt;

&lt;p&gt;The part I'm most proud of is the automation engine. It uses SQLite to track processed entities (no duplicate actions) and supports triggers like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Someone likes your note → auto-like their latest note back&lt;/li&gt;
&lt;li&gt;New note from specific authors → auto-like or restack&lt;/li&gt;
&lt;li&gt;New post from specific publications → auto-restack&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Auth without API keys
&lt;/h3&gt;

&lt;p&gt;Substack doesn't have a public API. Auth works by extracting your existing Chrome session cookies (AES-128-CBC decryption) or manual cookie entry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;postcli-substack auth login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @postcli/substack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;89 tests. CI on Node 18/20/22. Open source (AGPL-3.0).&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/postcli/substack" rel="noopener noreferrer"&gt;https://github.com/postcli/substack&lt;/a&gt;&lt;br&gt;
NPM: &lt;a href="https://www.npmjs.com/package/@postcli/substack" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@postcli/substack&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>cli</category>
      <category>mcp</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
