What it takes to build a software factory: part 1 - starting with a toy
For a long time, I'd wanted something simple: hand a piece of toil to an agent, close your laptop and go home. Come back the next day, see what happened.
Back in late 2025, folks in many companies had already been doing a version of this on their own machines, with Claude Code or Cursor staying open in a terminal for hours. But when they left for the day, the work stopped. Moving it off the laptop felt like the obvious next step.
Between June and mid-October 2026, Easel (the platform we ended up building at Miro) handled nearly 48,000 tasks: code reviews, alert triage, incident investigations and bug fixes, plus a metric tonne of other things I didn't expect.
My colleagues and I wrote about what it does on the Miro engineering blog. This blog series is the other half of the story: what it actually took to build, what decisions we made and reversed along the way, and what we learned.
Some of it went well. Some of it we're still paying for.
Wanting it before I could justify it
I work as a senior IC in Miro's platform engineering team, so "hosted agents" had been rattling around in my head for a while. We'd tried the cloud agents that vendors offer, and they were fine as far as they went, but they didn't have our context. They made great demos, but they couldn't reach the internal tools and systems that make a hosted agent useful for real work. They lacked the flexibility that comes with owning something from the ground up.
Other teams had noticed the same gap. A few had built their own automations, some on Cursor, some on scripts and cron, but none of them had stuck beyond the team that built them. This is hardly surprising - a team building a tool for itself has no reason to run it for everyone else.
So I did what I usually do when I can't stop thinking about something - I built a version of it at home.
FleetLift
FleetLift started in January as a personal open source project. It was based on the same concept as a tool from my previous employer, Skyscanner. They use turbolift to make deterministic changes across a lot of repositories at once. It works well, helping platform teams make updates across their whole fleet of services.
But what happens if the change is too fuzzy to script, and too repetitive to do by hand ? What if you could point agents at fifty, or five hundred repositories, giving each a set of instructions that also take into account the context from the codebase they're touching ?
It's a solvable problem, but not one that tends to end well on a developer laptop.
Guilty as charged: I'm a platform engineer, so naturally I think... what we need is a platform.
The catch with a personal project is that it can't prove much. I had no real scale to test against and no users to get feedback from. It was a toy, built around a problem I had at work but couldn't test there.
Building it did teach me a few things. FleetLift used OpenSandbox as the agent runtime, a durable execution layer built on Temporal (a workflow engine that keeps long-running jobs alive through crashes and restarts), a schema and database for recording agent output, and an idea of predefined "profiles" that tailored the agent for each task.
At its centre was a DAG (a directed acyclic graph): each job was a fixed set of steps in YAML, each with its own agent prompt, run in parallel where it could across as many repos as you pointed it at.
And then we wait...
Not much happened. A few months went by, things got busy, and the FleetLift idea languished.
Then, one day in June, a product team at Miro showed us a Slack bot they'd built on NanoClaw. It took requests from their team channel, ran an agent against them and replied back.
My reaction came in three parts, roughly in this order:
- This is cool. People were using it, and that shows real demand.
- This is NanoClaw... running in production. That's a security conversation nobody wants to have.
- It's not going to scale. It ran on a single node, using local Docker and SQLite, no telemetry, no CI and no way to verify a change. It certainly wasn't going to run on Kubernetes, where Miro's compute lives.
None of this is meant as criticism of the team. They're a product team, they built what they needed, and they had no interest in owning it for the whole company.
This conversation was the push I'd been waiting for. They became our first stakeholders: their use cases and their UX became the requirements for the first version of Easel, and FleetLift was the reference architecture.
A prototype is worth a thousand words
I didn't write a proposal and I didn't ask for permission. Building shared infrastructure is my job, and a working prototype is harder to argue with than a document. With AI, it's now realistic to build a functional solution before the meeting where someone would have said no.
Our meeting with the Slack bot team happened on 16th June, the first commit for Easel landed on 18th June. By the end of that day, an agent running in a sandbox in the cluster had cloned a repo, made a change and opened a draft pull request. Two weeks later, the project had received 520 commits and 69 merged pull requests covering a Slack integration, a GitHub integration, a web UI and an MCP catalogue. Of course, I didn't type most of that code - local agents did. But a human still reviewed every change before it merged, with AI reviewers giving us a steer on where to focus. That workflow deserves its own post, and I'll come back to it at the end of the series.

That first version cut a deliberate corner: the agent's GitHub token and model API key sat inside the sandbox as environment variables, because I wanted to prove the whole path worked before anything else. The security design was written up ten days later. Within a few weeks, credentials were fully removed from the sandbox and injected on the way out. Part 2 covers how that works.
Ben and Cole turn up
Around the same time, Ben Kramer (one of Miro's heads of engineering) had picked up an ambiguous task to establish a "software factory" that could autonomously triage and fix bug reports. He needed a platform to build on, and Easel had just appeared, so we joined forces. Nobody decided in a planning meeting that Easel would be the thing. It turned up at the right time and people started using it.
The detail I like most is in the git history. Ben's first commit was six days after mine and it wasn't platform code. It was a new preset for a use case he cared about. Cole Hoffer is a senior staff AI engineer at Miro. His first commit, a couple of weeks later, was the same: a new preset.
The platform was nowhere near done, but if new contributors could bring their own ideas and use the platform without changing the core, it probably had the right shape.
What I kept from FleetLift
A lot of FleetLift came across almost unchanged: the sandbox client, the Temporal scaffolding, the data schema, per-run GitHub tokens scoped to a single repo, and the habit of running the agent as a non-root user. "Profiles" turned into "personas" and then into "presets". FleetLift's preflight checks turned into skills.
The idea I'm most glad we kept is a small one with big consequences. FleetLift's control plane exposed platform capabilities to the agents it runs as MCP tools, and Easel does the same. Regardless of the harness running inside the sandbox, an agent can call back into the platform through tools the platform owns.
Memory was one of the first things we built on it. FleetLift called it the knowledge loop: agents record what they learn, a person curates it, and later agent runs fetch the lessons that apply to their task and give feedback on whether it helped.
Once the surface existed, it kept finding new uses. Agents now use it to start child runs and collect their results. Reflection agents use it to read the traces of previous runs and propose improvements. Code review uses it to record findings as structured records rather than prose. Each tool is narrow and authenticated for a single run. The control plane uses the preset definition to decide which tools an agent should see. Giving agents a new capability means adding a tool rather than handing them a credential.
The DAG was the first thing to be dropped. It works well for deterministic workflows, which is exactly what you want when you're running the same change across fifty repos. It's far too rigid for a general-purpose bot that might be asked to review a PR in one thread and investigate an alert in the next. When a human triggers the work, you mostly don't need a predetermined workflow. You can let the agent decide what to do and have the platform own the guardrails: what it's allowed to touch, what credentials it gets, and what shape the result must take and where it gets delivered. We kept a handful of deterministic workflows for specific jobs (smoke tests, mostly) but they're the exception.
Presets
If I had to keep one decision from the first week, it's this: there is one agent, and everything that tailors it to a task lives in reviewed configuration. We call that configuration a "preset". A preset pulls together the instructions, skills (packaged instructions and scripts the agent can load for a particular job), MCP servers, credentials and permissions a task needs. It lives as YAML in code, so changing the agent's behaviour means opening a pull request.
That sounds like bureaucracy but in practice, it meant anyone could add a use case without understanding the platform. Within the first month, people outside the core team had contributed presets for on-call duty, alert triage, cloud security investigations and reviewing PRs against a catalogue of known bug patterns.
My favourite early example came from Noel Varanda. His team already had a Slack support playbook written as a skill in their own repository. He pointed an Easel preset at it, so the skill stayed in one place, then guarded the interactive parts so they wouldn't misbehave when no human was around. Over the next few days he tuned the tone himself ("2-sentence replies, no tables, no landmines" is my favourite commit message of the summer), switched it on in his team's channels and later added an integration with the Braze MCP server. The platform team didn't change anything and that's exactly how it's supposed to work.
To date, roughly 27 people outside the core team have contributed presets or agent configuration. We didn't run an onboarding programme and we didn't need to. Most of them found us because it was easier than building their own.
Baking things into the image (sorry)
The day-one architecture document said, in bold, that the agent image should contain the build environment and nothing else. Behaviour (skills, prompts, tools) would be uploaded when a sandbox started, so changing a prompt wouldn't mean rebuilding an image.
I broke that rule the same afternoon. Agents couldn't verify their own changes without basic tooling, so I baked a toolkit into the base image. Soon after, skills started going into images too, and that was the rule properly broken.
Two weeks later, we built an image with a cloned copy of Miro's client monorepo along with the toolchain needed to work with it. That repository is the codebase behind the Miro app in your browser - it's big enough that cloning it and installing dependencies on every run took minutes. That image is around 4 GB. The day after we started using the new image, we hit provisioning timeouts because the sandboxes were taking too long to spin up.
Each step made sense on its own but together, they rebuilt exactly the slow build cycle the architecture rule was meant to prevent. Skills and tools changed daily and every change meant a time-consuming image build.
Getting back to our original principle - "upload behaviour at launch" - took until October. It was a painful migration because by then we were running hundreds of agent sandboxes in production, and any change in agent behaviour is noticeable.
The principle was right on day one but I didn't hold the line on it when shipping got in the way.
Our own cluster ? Great.
Our own everything else ? Less great.
One more decision made the speed of delivery possible, and it's the one we're still paying for.
Miro had an isolated EKS cluster already set aside for AI tooling, and Easel moved in as the first customer. Isolation is what let us run agents we didn't trust inside a company that (rightly) cares a lot about security and governance. If an agent went wrong, it would do so somewhere that couldn't reach production. Isolation also meant the team could move fast.
The bill came later though. Miro platform engineering provides product teams with a lot of infrastructure they never have to think about: Terraform modules for AWS RDS, managed Temporal Cloud, ArgoCD for deployments, GitHub runners with the right permissions to change AWS resources safely, and an observability stack that just works. Because our cluster was different from every other cluster, none of that applied to us and we had to build our own solutions on top of the raw building blocks:
- No RDS ➡ we ran Postgres in the cluster.
- No Temporal Cloud ➡ we self-host Temporal.
- No ArgoCD ➡ we wrote our own deployment pipeline on GitHub Actions.
- No observability stack ➡ we ran our own until the observability team caught up.
Reader, it's still running in production today 🤦♂️
We ended up being platform and infrastructure engineers far more than we should have been. I'd still make the same call, because the isolation is what made the first few months possible.
What it looks like now
Today a request from Slack, GitHub, Jira, incident.io or a cron schedule hits a gateway, gets resolved against its preset into an immutable run specification, and is handed to Temporal.
A separate worker component, driven by a Temporal workflow, creates a fresh Kubernetes pod using a microVM (a lightweight virtual machine, so each run gets its own kernel and can't see any other run). It uploads the skills and tools the preset asks for, and drives the agent through a harness adapter (the harness is the agent loop itself, such as Pi or Claude Code) so we can swap harnesses without the rest of the platform caring.
Every network request and credential goes through a gateway outside the sandbox. Every turn and tool call is stored. The result the agent writes, not its log output, is what gets delivered.
Easel's own code is one Go binary, running as gateway, worker, API and UI.
So, was it hard ?
Honestly, no. Building it was easy, and that's what surprised me most about the first few weeks. With agents writing the code, a cluster we could experiment with, and FleetLift to copy from, the distance between an idea and a running feature was often an hour or two.
Running it was a different story.
This is part 1 of a series:
- Starting with a toy (this post)
- How do you give an agent real access without trusting it?
- How do you know it's any good?
- What happens when agents build the agent platform?
Thanks to Ben Kramer & Cole Hoffer who helped turn a prototype into a platform.