Why AI agents are insanely productive on Elements
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. ```bash 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. ```bash Terminal elements create migration "add newsletters table" -tables=newsletters ``` ```sql 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. ```ts 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. ```ehtml 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. ```ts 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. ```bash 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.
Hey guys, in this video I want to talk a bit about why Elements makes agents so productive and what it means to have a full stack. So an agent by itself is actually quite good at writing code and it's very good at applying different recipes and kind of different templates to problems. And so if you were going to build an application that had some database tables and you wanted to have let's say a real-time comment system or some other features, the agent can kind to stitch that together and figure it out, but it's going to have to reinvent a lot of things from scratch that wastes a lot of time and tokens. Conversely, or alternatively, it might have to go out and get a bunch of open source packages that it can try to use to stitch together to create your application.
And those open source packages are often made by volunteers and are not necessarily made to work together. On top of that, there's no build tooling that tells the agent whether or not what it's doing is correct or not. And so it'll have to stitch or add on something like that. Elements creates an environment that's completely combined into one coherent tool system. So the agent can just focus on building your application, not stitching together building blocks or build server, things like that, that helps it make progress. So with the rest of the video, let me walk through some of those things that it has that makes the agent so productive. So first of all, when you create an Elements application, a server gets started in the background automatically.
And that server is always running. That's actually, to my knowledge, the first build tool out there that works this way. And it's important because your agent, whether it's one agent or many agents, sometimes these days we're working with maybe 10, 20, as many agents as you want working on your app at once, can plug into the same server and get the same exact build results all at once, and your editor can plug into that same server as well. So typically, if you're pulling up Visual Studio Code, working in Vim, whatever you're working in, it would have its own TypeScript server, for instance, that's running separately from your build process. And then every agent that's working on your app would run its own type checker and its own build process, its own database migrations.
None of them would be coordinating together. So you really need a project server, a central server that coordinates all of the things related to building your application. So that project server is always running. The second thing it has is TypeScript built into it. So we've actually brought in the TypeScript source code from Go into the Elements toolchain, and we've modified it somewhat to use our own source graph to make it multi-target so that it works with both server and browser targets. So we're going to type check your code for both the server and the browser. And we want it to make it extremely fast. And so it's significantly faster than just using the regular tsgo command line tool.
And that's because we've overdone, we've overhauled the way that they do source graphs and the way that it gets its information comes from our project server. And we keep it up to date, we keep it in memory in real time. So we have type checking. When an agent asks for the type checking results, the type checking results have already been computed, and that's because as soon as you save a file, whether it's the agent saving the file or you're saving the file in your editor, the build happens automatically. Type checking happens as part of that build. Typically for a single file save, it might be one to five milliseconds just to do all of that, the entire build for both the server and the browser target. That includes There's type checking, program analysis, emitting, bundling, all the things that have to be done.
And by the time the agent asked whether or not it made any mistakes, that work has been done. And so the result will come back in roughly 20 to 50 microseconds, very, very quick. So the project server is built such that it can respond almost in real time as the agent is working. And this empirically has shown us that it makes a big difference. So the agent can just keep working, working, working, working without doing a lot of follow-up work to see whether what it created is actually working. Okay, so the project server provides microsecond level feedback to the agent. This is also useful in your editor. If you're working in your editor, you might be used to seeing your TypeScript errors show up in your editor.
You'll get that with Elements as well. But the project server is serving your editor and all the other, the tooling and the agents from the same place. So that'll save memory and CPU as well. Okay, so the project server then is providing type checking, program analysis, it's emitting all in real time. And then there's a bunch of tools that are built on top of that that the agent will use. So let's look at a typical feature that you're going to build in an application. Maybe it's a newsletter that you're trying to bring in-house. You want to build a paid newsletter. Well, you tell your agent you want to build a paid newsletter and the first thing it's going to have to do is to model some tables in the database.
And to do that in Elements, it can do Elements, create migration, and that will create a migration file that it can then write in raw SQL, it doesn't need to use any ORM, or an abstraction in a framework, it just writes SQL. And agents are extremely good at writing SQL, there's a ton of training data for SQL. So it will write your migration in SQL, and that, once it's saved, will be synced automatically with your database. And so the agent doesn't have to do something like elements db migrate, like we used to have to do with Rails or some of the other frameworks that are out there And it doesn't have to add on another tool to do database migrations It's just baked in and it happens at the speed of file save while you're working if you want to change the migration you can just change it and it will patch the Database in real time. And so when you write a database migration, it might add 50 to 100 milliseconds to your build But it all happens in a blink of an eye you don't even really think about it.
If there's any errors in the SQL, it will fail the build and the agent will find out about that right away. So database migration errors become part of the same build errors that are where you see type checking errors, program analysis errors, test errors, all of that comes back in the same report to the agent. So once it's done creating those database migrations, typically it would move on to creating some logic, some functions on the server that would access that database, either to pull data to load the page or perhaps to write a newsletter and to save it to your database. And typically frameworks have all kinds of abstractions to make this dance work effectively.
We took multiple years to build this such that it's just completely seamless. We wanted the abstraction of calling something on the server to simply be you call a function. As humans, we're very good at calling functions. We learned that that's one of the first things we learn in programming is how to do that. It's obviously very easy for an agent to do that as well. And so you just write a function and you give it a special annotation that looks like a JSDoc comment that says @rpc. We call them build tags, but they look kind of like JSDoc tags. You say @rpc, and that marks the function as a remote procedure call or a function You can think of that can be invoked from the browser over the wire to the server.
What's really cool about this is one is you really don't have to learn anything. We already know how to call functions and return values. It's secure automatically, so you don't have to worry about that. All the types of things that you might worry about with this have been thoroughly resolved. And so remote execution, things like that are not really an issue here. You just call the function with data as arguments, and you get data back over the wire, that's it. And so a function is just made by an RPC function by just putting a comment up at the top. So the agent can do this very, very quickly. Now, inside that function, you can call a SQL function, you can call the SQL function and just write raw SQL.
And again, agents are really, really good at writing raw SQL. And when they call the SQL function, they type the SQL query, and they just don't even worry about SQL injection because our compiler takes care of that for it, so it just writes it as a plain template string, putting the parameters directly into the string, and then the compiler takes care of making sure that it's safe so that there can't be any injection. The compiler also does program analysis to make sure that the SQL is not called directly from the browser, so the browser has to go through one of these RPC functions in order to call SQL, otherwise it just simply won't compile. And then also inside of these RPC functions, we have a very simple primitive called session that the agent can work with to determine whether the user is authenticated or not, or to log in the user or log them out.
And so it's some very simple primitives, maybe four or five primitives that the agent can work with. And from that, it can build all sorts of complicated backend logic. Now, the front end, the user interface, the agent needs to work with something that's reactive, a language that allows it to build complicated interfaces very quickly. And we want to build that. We built our own HTML language. It was many years in the making, and it actually started back in 2012. And the point of making that was to push down all the complexity of the reactivity and and getting things up to date away from the user and the syntax and down into the runtime. So when you're building these interfaces all you have to think about is the HTML markup. We didn't design a separate language we made it HTML completely with some light syntax extensions that allow you to do TypeScript expressions, constructor attributes, and so on. And then the data functions just like function parameters and they're made lexically scoped and automatically reactive.
So the user interface will update automatically, our runtime will do that, and it does it as fast as possible. So the agent can move very, very fast to this because it already knows how to write HTML, it already knows how to do function parameters, and it just gets told don't worry about the data reactivity. That will just happen for you. And it can factor out the templates to create reusable templates really, really easily, and it will actually do that. So it makes your code more maintainable. It's fast to write, and it's very performant in the browser. The runtime takes care of making it performant. So now you have something that has, in the application framework, from the user interface to the backend functions to the database, it's all integrated to work together, and the agent can make ferocious forward progress.
And the project server makes sure that it is not making any mistake, and it's giving it guidance on how to fix its mistakes, if it does. The final part of this is it's going to want to write some tests. Now, typically tests have been kind of a thorn in our side as developers, and people have different viewpoints on them. I personally like to write tests if it's a big application for things that could regress over time, and I also like to write tests if it helps me to build a feature. I don't overdo it, but I do think having tests, particularly if you're in a team or you're building a complicated system, you really do have to have tests.
But historically, tests have been really slow. They're slow to run, they're kind of annoying to write. If you don't write them correctly, they can sometimes hose things up and slow down the development cycle. But I found that with agents, that has changed a lot. So agents really want to write tests. It's generally a good idea to write tests because it helps them to figure out whether they broke something that they built a couple of weeks ago or months or years ago. So it helps you to prevent regressions in your application. And what we need to do on the Elements side is to create a test runner that's extremely fast. It keeps the test results in memory, only reruns the test that have to be rerun based on the things that affect them.
And when the agent asks for the test results, it gets those results in microseconds, just like type checking or any of the other build results that come back. So writing tests in Elements is extremely fast for the agent to do, and it also gets the results back extremely quickly. Next up, we have to get this code somehow to production. We have to deploy it onto a remote machine. And that's a lot of orchestration to do safely. And what we do with Elements is we take control of that process. We can work with any Ubuntu machine, any Linux Ubuntu machine. We provision the machine. We set it up automatically. We install whatever software needs to be installed.
And once it's up and running, which usually takes about 60 seconds the first time, a hot deploy will typically take anywhere from half a second to maybe five seconds on a slightly slower box. And the reason it's so fast is that we connect peer to peer, project server to project server. And if the build is failing locally, it won't even deploy at all. Once it connects for a deployment, it starts to run the deployment in sequence across one or as many machines as you have. You could have a cluster of five machines if you want. They're all load balanced automatically. SSL is done automatically, and the deployment will go through on each box the first part of the build, which is the type check, the program analysis, the basic compilation, and then it will run the database migrations, if there's any, to be run on the test database first.
We always have a test database in addition to the app database for every single application, so your migration will be run on the test database. If they fail on the test database, the whole deploy stops. If they don't fail, we'll go ahead and run the tests and the tests run on only the test on the code that's in the build folder not your release and that's important because We're building up our way towards a safe release and your folder that's running and serving your applications Or serving your users is left alone until we're pretty sure that everything's going to be okay Pretty sure meaning we're almost a hundred percent sure Okay, so the tests run, now they run very quickly.
We have up to four, five, eight, we can have as many test runners as we want, but typically we have as many as there are CPUs. And so there's gonna be tests running concurrently, takes a couple of milliseconds. If all of those pass, the database migrations will now run on your application server, or on your application database. And typically migrations run on your production app database in a couple of milliseconds. For ones that are longer, you can put up a maintenance page during that process if you want, but usually we don't do that for quick changes. By default, they'll run in a transaction, so if there's any errors, the whole thing will roll back for each migration.
And once that's done and succeeded, we will flip the switch on the release that atomically does the final release, and then in memory hot reloads your server on Node .js and sends a signal to the browsers to hot reload themselves as well. Now the first time you do this, I guarantee you it will feel magical. If you're working with an agent, you can talk to the agent on your phone and you can say make this change to the home page and deploy it and boom, in a second or two later it's up on your phone. It's an incredible way to work. Having all of this together as a toolset that the agent can use to build applications is an absolute superpower.
And once you get used to working this way, I don't think you'll ever want to go back to anything again. Everything from being able to build the feature to deploying it and having it in production is completely integrated tightly to work well together. And so you can really safely build very complex applications from the prototype where you're just building something quickly in a couple of hours to just continuing to build with it from days to weeks to months and years on the same exact code base across many different agent sessions. And I think you can feel much, much more confident that you're going to have an application that's maintainable and built the correct way, because Elements provides the guardrails, the project server, and the framework to make it all consistent.
So I hope this gives you a better sense of how Elements helps guide agents, makes it a kind of superpower for agents to be super productive, but I think it's also great for humans. And so if you, for whatever reason, are just using agents occasionally, or if you want to get in there and write the code yourself, you can also do that. So you can feel safe that even if you let agents run loose for a couple of months, and then you say, hey, I want a human team to come in and work on this for a bit, you can just hand it off, and it will be a good, maintainable code base. All right, let me know if you've got any questions in the comments.
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