Here is a Birkly template:
{for each entry in 'blog' ordered by newest limit 3}
And here is roughly the same instruction in a conventional template language:
{% for post in posts|sort(attribute='date')|reverse|slice(0,3) %}
The second is shorter. It is also, if you have not used that particular language, a small puzzle. Which attribute is it sorting on? Is slice inclusive? Does reverse apply before or after the slice? You can work it out. The question we kept asking is why anyone should have to.
Terseness is a bet about who reads the code
Compact syntax makes sense under one assumption: the person reading is the person who wrote it, and they do this every day. Under that assumption the pipe chain is elegant — dense, expressive, fast to type once fluent.
That assumption is almost never true for a CMS template. Templates are read by the developer six months later, the contractor who inherited the project, the designer trying to move a block, the client's in-house developer taking over maintenance, and — new arrival — an AI agent asked to add a section.
Only one of those readers benefits from terseness. Every other one pays for it.
Template code is written once and read for years. Optimising the write is optimising the wrong end.
The design rules
Three constraints, applied consistently, produce most of the syntax.
Read as a sentence. for each entry in 'blog' ordered by newest limit 3 can be said aloud to a non-programmer and understood. This is a real test we apply, not a metaphor. If a clause cannot be read aloud sensibly, the clause is wrong.
Name the thing you mean. ordered by newest rather than a sort function plus a direction flag. The intent is what you write; the mechanics stay inside the engine where they belong.
No hidden control flow. A block starts where it says it starts and ends at its endfor or endget. There is no inheritance chain, no implicit context injected from a parent template, no filter that quietly changes what the loop variable means. What the file says is what happens.
The result is more characters. We think this is the right direction to be wrong in.
Two forms, two jobs
Almost everything is one of two constructs.
{for each entry in 'collection'} … {endfor} iterates. Order it, limit it, filter it with a with clause.
{from 'collection' get entry 'slug'} … {endget} fetches one thing. A single-entry page works from URL context instead: the page declares its collection, and one blog-post.html serves every post in it. There is no page per entry, no regeneration step, no deploy when you publish.
Fields come out as {'field_name'}. Formatting is stated where it is used: {created_at format "F j, Y"}. Two constructs and a field syntax cover the overwhelming majority of real templates, which is the point — the language is small enough to learn in an afternoon and finish learning.
The reader nobody designed for
We wrote these rules for humans. The agent benefit was a consequence we did not anticipate, and it turned out to be substantial.
Ask an AI to add a section to a template in a language built on filter chains, and it has to reason about semantics that live in that language's documentation rather than in your file. It usually gets there. Sometimes it produces something that has the right shape, uses a filter that does not exist in your version, and fails at render.
Plain-English syntax collapses that problem, because the instruction and its meaning are the same string. An agent reading a loop written as an ordinary sentence does not need to know Birkly to know what it does. There is no schema briefing, no convention to look up, no version-specific filter table.
This is the same argument as the one for plain PHP under the hood, arriving at the template layer. Simplicity is not a concession to beginners. It is what makes a system legible to any reader, and the set of readers just got larger.
The honest costs
It is more typing. If you write templates all day, you will occasionally want the pipe chain. We accepted that.
It is less powerful at the edges. There is no arbitrary expression evaluation in templates, which means some transformations that would be a one-liner elsewhere need to happen in the content model or a plugin instead. That is deliberate — templates that can compute anything become templates that contain business logic — but it is a real constraint and you will hit it eventually.
Errors surface at render. There is no compile step, so a misspelled field name fails when the page loads rather than when you save. Clear error messages help; a type checker would help more.
What we were actually after
The goal was a template a designer can modify without asking a developer, a developer can inherit without a handover document, and an agent can extend without hallucinating. One syntax, three audiences, no translation layer between them.
You can read the templating reference in the docs. Birkly will be free and open source at release — join the waitlist for alpha access.