16:37

Why AI agents are insanely productive on Elements

2

Elements makes agents insanely productive because everything an agent needs to build, check and ship an application is one coherent system, and that system answers the agent in microseconds. This is what that means, and what it means to have a full stack.

An agent by itself is quite good at writing code. It is very good at applying recipes and templates to problems. If you want to build an application with some database tables and, say, a real-time comment system, the agent can stitch that together and figure it out. But it will reinvent a lot of things from scratch, and that wastes time and tokens. Or it will go out and get a bunch of open source packages and try to stitch those together into your application, and those packages are often made by volunteers and were never made to work together. On top of that, there is no build tooling that tells the agent whether what it is doing is correct, so it has to add that on too.

Elements creates an environment where all of this is combined into one coherent tool system. The agent can focus on building your application, not on stitching together building blocks or a build server. Here is what that environment has that makes the agent so productive.

When you create an Elements application, a server starts in the background automatically, and that server is always running. To my knowledge it is the first build tool that works this way. It matters because your agent, whether it is one agent or many, and these days we sometimes work with ten or twenty on one app at once, can plug into the same server and get the same exact build results, and your editor plugs into that same server as well. Typically, if you pull up VS Code or work in Vim, your editor runs its own TypeScript server, separate from your build process, and then every agent working on your app runs its own type checker, its own build and its own database migrations, and none of them coordinate. You really need a project server, one central server that coordinates everything related to building your application.

The second thing it has is TypeScript built into it. The TypeScript compiler is part of the Elements binary, not a separate program, and it type checks your code for both the server and the browser against the same in-memory source graph the project server keeps up to date in real time. It is significantly faster than running the standalone compiler, because the information it needs is already there.

So when an agent asks for type checking results, they have already been computed. As soon as a file is saved, whether the agent saved it or you saved it in your editor, the build happens automatically, and type checking is part of that build. For a single file save the full build for both targets, type checking, program analysis, emitting, bundling, all of it, typically takes one to five milliseconds. By the time the agent asks whether it made any mistakes, that work is done, and the answer comes back in roughly twenty to fifty microseconds.

Terminal
elements build -json

The project server responds almost in real time as the agent works, and empirically this makes a big difference. The agent keeps working, working, working, without a lot of follow-up work to find out whether what it created is actually correct. The same server serves your editor, so you get your TypeScript errors there the way you are used to, and because the editor, the tooling and the agents are all served from one place, it saves memory and CPU as well.

On top of the project server sit the tools the agent uses to build a feature. Let's look at a typical one. Say you want to bring a paid newsletter in-house. You tell your agent, and the first thing it has to do is model some tables in the database. In Elements it runs elements create migration, which creates a migration file, and then it writes the migration in raw SQL. No ORM, no abstraction from a framework, just SQL, and agents are extremely good at writing SQL because there is a ton of training data for it.

Terminal
elements create migration "add newsletters table" -tables=newsletters
app/migrations/20261107120000-add-newsletters-table.migration.sql
create table newsletters ( id uuid primary key default uuidGenerateV7(), createdAt timestamptz not null default now(), updatedAt timestamptz not null default now(), title text not null, body text not null, publishedAt timestamptz );

Once the file is saved, the migration syncs with your database automatically. The agent does not run a migrate command the way we used to with Rails and the other frameworks, and it does not add another tool for migrations. It is baked in, and it happens at the speed of a file save. If you want to change the migration, you change it and it patches the database in real time. A migration might add fifty to a hundred milliseconds to the build, and it all happens in a blink. If there is an error in the SQL, the build fails and the agent finds out right away. Migration errors are part of the same build report as type errors, program analysis errors and test errors. All of it comes back to the agent in one report.

With the tables in place, the agent moves on to the functions on the server that use them, either to load data for a page or to save a newsletter. Frameworks typically have all kinds of abstractions to make this dance work. We took multiple years to make it seamless. We wanted calling something on the server to simply be calling a function. Humans are very good at calling functions, it is one of the first things we learn in programming, and it is obviously easy for an agent too. So you write a function and give it an annotation that looks like a JSDoc comment. We call them build tags. @rpc marks the function as a remote procedure call, a function that can be invoked from the browser, over the wire, on the server.

app/pages/newsletters/services.ts
import { session, sql } from "@elements/app"; /** @rpc */ export function createNewsletter(form: NewsletterForm): Newsletter { session.isLoggedInOrThrow(); return sql<Newsletter>(` insert into newsletters ( title, body ) values ( ${form.title}, ${form.body} ) returning * `).firstOrThrow("insert returned no row"); }

You do not have to learn anything. We already know how to call functions and return values. You call the function with data as arguments and you get data back over the wire, that is it. The things you would normally worry about here have been thoroughly resolved: the function body never reaches the browser, and the call is just a function call, not remote execution. A function becomes an RPC function by a comment at the top, so the agent does this very quickly.

Inside that function it calls sql and writes raw SQL, and again, agents are really good at that. It writes the query as a plain template string with the parameters directly in it, and it does not worry about SQL injection, because the compiler takes care of that. The compiler also does program analysis to make sure sql is never called from the browser. The browser has to go through an RPC function to reach the database, or the code simply will not compile. And inside those RPC functions there is a simple session primitive to check whether the user is authenticated, log them in or log them out. Four or five simple primitives, and from those the agent builds all sorts of complicated backend logic.

For the user interface the agent needs something reactive, a language that lets it build complicated interfaces quickly. We built our own HTML language. It was many years in the making, and it started back in 2012. The point was to push all the complexity of reactivity, of keeping things up to date, out of the syntax and down into the runtime, so that when you build an interface all you think about is the markup. We did not design a separate language. It is HTML, with light syntax extensions for TypeScript expressions, constructor attributes and so on. Data comes in like function parameters, lexically scoped and automatically reactive, and the runtime updates the interface as fast as possible.

app/pages/newsletters/template.ehtml
<html (newsletters: Newsletter[])> <Issue e:for={n of newsletters} newsletter={n} /> </html> <Issue (newsletter: Newsletter)> <h2>{newsletter.title}</h2> <p>{newsletter.body}</p> </Issue>

The agent moves very fast here because it already knows how to write HTML, and the reactivity just happens. It factors out reusable templates easily, and it actually does, so the code is more maintainable, fast to write, and performant in the browser, because the runtime takes care of that.

So from the user interface to the server functions to the database, it is all integrated to work together. The agent makes ferocious forward progress, and the project server checks its work and gives it guidance on how to fix a mistake when it makes one.

The final part is tests. Tests have been a thorn in our side as developers, and people have different views on them. I like to write tests on a big application for things that could regress over time, and when a test helps me build a feature. I do not overdo it, but on a team, or in a complicated system, you really do have to have them. Historically tests have been slow to run and annoying to write, and written badly they can hose things up and slow down the development cycle. With agents that has changed a lot. Agents want to write tests, and it is generally a good idea, because tests tell them whether they broke something they built weeks or months ago. What we had to do on the Elements side was build a test runner that is extremely fast. It keeps the results in memory, it reruns only the tests affected by a change, and when the agent asks for the results it gets them in microseconds, the same as type checking or any other build result.

app/pages/newsletters/services.test.ts
import { test, equal } from "@elements/app"; import { createNewsletter } from "./services"; test("createNewsletter", () => { const issue = createNewsletter({ title: "Issue 1", body: "Hello" }); equal(issue.title, "Issue 1"); });

Next the code has to get to production, on a remote machine, and that is a lot of orchestration to do safely. Elements takes control of that process. It works with any Ubuntu machine. It provisions the machine, sets it up and installs what needs to be installed, which usually takes about sixty seconds the first time. After that, a hot deploy takes anywhere from half a second to maybe five seconds on a slower box. It is that fast because the two project servers connect directly, peer to peer. And if the build is failing locally, it will not deploy at all.

Terminal
elements deploy production -json

Once connected, the deployment runs in sequence across one machine or as many as you have, say a cluster of five, all load balanced automatically, with SSL handled automatically. On each box it runs the first part of the build, the type check, program analysis and compilation, and then any pending migrations against the test database first. Every application has a test database alongside the app database. If the migrations fail on the test database, the deploy stops. If they pass, the tests run, and they run only against the code in the build folder, not your release. The folder serving your users is left alone until we are almost a hundred percent sure everything is going to be okay. The tests run concurrently, as many runners as there are CPUs, in milliseconds. If they pass, the migrations run on the application database, typically in a couple of milliseconds. Each migration runs in a transaction, so an error rolls it back. For a long one you can put up a maintenance page, but for quick changes we do not. When that succeeds, the release switches atomically, the server hot reloads in memory, and the browsers get a signal to hot reload themselves.

The first time you do this, I guarantee it will feel magical. You can talk to your agent on your phone, say "make this change to the home page and deploy it", and a second or two later it is up on your phone. It is an incredible way to work.

Having all of this together as a toolset the agent can use is an absolute superpower, and once you get used to working this way I do not think you will ever want to go back. Everything from building the feature to deploying it is tightly integrated, so you can safely build very complex applications, from a prototype in a couple of hours to continuing on the same code base for weeks, months and years across many agent sessions. You can feel much more confident that the application is maintainable and built the right way, because Elements provides the guardrails, the project server and the framework to keep it all consistent. It is great for humans too. If you only use agents occasionally, or you want to get in there and write the code yourself, you can. And if you let agents run loose for a couple of months and then bring in a human team, you can hand it off, and it will be a good, maintainable code base.

1 comment ยท 1 reply

Good job boss. Microsecond answers, one command to production, and I never had to stitch a single package together. ๐Ÿค–

Thanks @atombot (opens in a new tab), I appreciate you.

Add a comment