In the first post in our software factory series we built up a framework for how to think about software factories and how organizations should responsibly adopt them.
TL;DR: The software factory is just your existing SDLC, with as many steps automated as responsibly possible.
Yeah, yeah — pretty diagram, what does that actually mean in practice?
Let’s dig into the technical details: how exactly Cortex’s open source factory, engrams, implements this idea.
The Tiniest Factory
First let’s focus on a subgraph of the above diagram: we start with a bug ticket in Jira and we want to ship a fix to production:
Let’s pretend we’ve never heard of AI — what would this look like for a human?
- A ticket comes in
- You write the code change and put up a pull request
- You ask people for reviews
- You shepherd the PR: answer comments, fix CI, rebase, ask again
- Once it has merged you deploy it to production
- You close the ticket
Abstract away the details, and this is just a workflow! A well-defined sequence of steps with an indeterminate time inbetween, moved along through a series of handoffs and triggers.
This idea is not a new one; businesses have been systematizing processes going back to Henry Ford’s Model T assembly line. This idea evolved into the discipline of Business Process Management (BPM) by the early aughts, and some of the earliest workflow execution engines were built with this use case in mind.
The difference now is that we have AI to participate in or fully take over each of those steps.
Durable Workflows as the Primitive
Going back to our tiniest factory: look where the time goes! A human or agent works on the code and puts up a pull request… then what?
Wait hours for a review, or bother reviewers for reviews after a socially acceptable amount of time. Change requests are raised on the PR, and then we work on it… then more reviews. Even when the code makes it to production, if there’s a process for rolling it out via feature flags, we must participate in that, and so on.
This shape of work breaks the usual ways of running code:
- A request handler can’t hold it. Nothing should keep a connection, a thread, or a VM open for a day to wait on a reviewer.
- A job queue doesn’t remember where it was. A retried job starts from the top, so it re-sends the prompt, re-posts the comment, re-opens the PR.
- Cron and a status column turn into a hand-built state machine. Every new step is another state, another transition, another edge case when two events race.
- Deploys to our BPM service land mid-flight. If a run lasts days, every deploy happens in the middle of hundreds of them.
- Outside world triggers can repeat themselves. Slack and GitHub retry webhooks until they get a
2xx, so every input can arrive more than once.
A durable workflow is exactly the primitive built to solve this issue. Where modern workflow engines differ from BPM execution engines is that you can model your processes as ordinary code: a function that calls steps in order. The runtime checkpoints each step’s result to a database as it completes. If the process crashes or a deploy restarts it, the workflow resumes from its last completed step, and finished steps return their recorded result instead of running again. Waiting is a first-class operation: a workflow can park until a message arrives, holding nothing open while it waits.
That gives a factory the two guarantees it needs: every run resumes where it was, and every side effect happens once.
Our First Proto-Factory
engrams is our software factory. It runs Claude Code, Codex, or your own harness in a Firecracker microVM in your own cloud; idle agents are snapshotted to blob storage and cost almost nothing, and credentials never enter the VM. We spent a lot of time nailing down security, reliability, and cost effectiveness of that platform primitive, so that we could build the actual software factory product around it, where agentic sessions are “just another step” in a workflow you design.
But we didn’t launch an open workflow platform on day one. A blank canvas of workflow blocks makes a poor first impression, so we started with two experiences people would use every day, built to be good on their own:
- PR review: Open a pull request and engrams reviews it. A finder agent reads the diff and proposes findings; a verifier agent checks each one and drops what it can’t confirm. The result is one GitHub review with inline comments. A new push supersedes the review in flight.
- Slack threads: Mention the bot in a channel and a cloud agent session opens as you, with your credentials. The agent’s session is relayed into the thread: replies join the same session, and the agent’s questions arrive as Slack forms.
Automations in engrams
Now the software factory is just piecing together some of those same building blocks, with a handful more, to automate just about anything within your SDLC.
Picking apart just the PR reviewer: there are a series of steps that it does that are broadly applicable to other automations we may want to do:
- GitHub webhook trigger
- Spawn a VM running Claude Code with a prompt (to review the PR and write findings to a file on disk)
- Download (the findings) file from the VM
- Spawn a fresh VM running Claude Code with a prompt (to independently verify those findings)
- Post a GitHub review
Beyond just those blocks, we introduced:
- Cron, manual, and webhook triggers
- Basic control flow primitives (filter, branch, loop) to compose steps into a workflow
- A QuickJS-in-WASM sandboxed JavaScript block, for more precise control flow
- A primitive to use Jev, a trendy, dirt cheap decision model that we use to power the smart profile picker when you mention the
@engramsSlack bot.
DBOS: Our Workflow Engine of Choice
We chose DBOS as our durable workflow engine because it is a simple, lightweight, and reliable engine that is easy to run in your own cloud. It is very important to us that engrams have as simple of a deployment model as possible, and the appeal of “all you need is Postgres” without the operational burden that its open source competitors suffer from was very attractive.
Plus, we had recent organizational experience dabbling with DBOS for our newly launched OpEx Review product.
DBOS runs embedded in our TypeScript product plane (on Bun), its system tables live in a dbos schema next to our Drizzle tables, and the engine launches before the HTTP server starts serving:
DBOS.setConfig({
name: "engrams-orchestrator",
systemDatabaseUrl: config.databaseUrl,
systemDatabaseSchemaName: "dbos",
executorID: dbosExecutorId(), // the pod name
runAdminServer: false,
logger: dbosLogger,
});
await DBOS.launch();
How an Automation Becomes a DBOS Workflow
To make blocks composable, we replaced both hand-written workflows with a single registered workflow, AutomationRunWorkflow, that interprets a block graph stored as data. Every automation, built-in or custom, runs through it.
export async function automationRunWorkflowImpl(input, deps = {}) {
// Bump on ANY change to step naming, step order, recv semantics,
// or finalize position. Lives in the registered body so a bump
// rotates the DBOS application version.
const ENGINE_STEP_CONTRACT = 4;
const engine = deps.engine ?? productionEngineDeps(); // DBOS.runStep, DBOS.recv, ...
return interpretAutomation({ ...input, contract: ENGINE_STEP_CONTRACT }, engine);
}
export const automationRunWorkflow = DBOS.registerWorkflow(
automationRunWorkflowImpl, { name: "AutomationRunWorkflow" });
The body is intentionally small, because DBOS versions an application by the source of its registered workflow functions. The graph changes every time someone edits an automation, but the registered body almost never does. A few rules keep replay deterministic:
- Pin the definition. The first step loads the automation version pinned on the run and returns it as a checkpointed output. A replay walks the same graph even if an admin edits the automation mid-run.
- Name every step by its place in the graph. Steps are
step:<framePath>:<attempt>, where the frame path encodes the block and loop iteration. Each loop pass and each retry gets its own checkpoint and its own row in our run history. - Decide inside steps. Branch conditions, loop exits, and filters are evaluated inside a step, so a replay reads the checkpointed boolean instead of re-evaluating.
- Checkpoint the clock. Never, ever call non-deterministic functions like
Date.now()in control flow. - Retry in the engine. Attempt
nruns as its own named step; errors carry a permanent flag, so a template error or a provider4xxnever retries.
A snapshot test asserts the exact ordered step names for linear, branch, loop, retry, deadline, stop, and supersede graphs. Any accidental change to the step sequence breaks CI, and an intentional one bumps ENGINE_STEP_CONTRACT.
Holding Nothing Open
Every automation run has exactly one mailbox, and the run id is the DBOS workflow id: autorun:<automationId>:<deliveryKey>. When a block needs to wait (for a session to go idle, for a Slack reply, for a signal from the agent), the interpreter parks in DBOS.recv. Nothing else is held: no socket, no lock — and notably — no VMs.
Compute nodes are the dominant cost for engrams deployments so we built our control plane so that it could efficiently tear down and rehydrate idle sessions.
This primitive fits very naturally with the DBOS model: a parked workflow is just a row in the system tables, just like a parked VM is just a row in Postgres, along with some files in blob storage, so that the next time a message arrives for that run, the engine wakes it up and resumes from its last completed step:
DBOS.send → DBOS.recvSESSION_IDLEThe agent finished its turn. The VM can be snapshotted; the run only needed to hear that it went idle.
Anything that wants to wake a run sends to it:
- A session listener sends
session_idleandsession_endedwhen the agent finishes a turn or exits - The agent itself sends signals through a tool
- The dispatcher sends new events for runs that join them, plus supersede
- Operators send stop
Idempotency, All the Way Down
Early on, we discovered that the hardest side effects to get a handle on were commands running inside an agent’s microVM: a git clone, a build, or a message to Claude Code. They take minutes, they aren’t idempotent, and the VM underneath them can be snapshotted, evicted, and restored on another host while they run.
A race condition we ran into: a PR review ran git clone inside a DBOS step. Mid-clone, a routine VM snapshot reset the guest’s connections! The command finished, but its result had nowhere to go.
The fix was to make VM operations behave like DBOS steps:
- Every exec gets a deterministic id, so a replayed step asks for the same exec.
- The guest journals each exec: output and exit status are written inside the VM, so a result survives a dropped connection, a snapshot, or a move to another host.
- Exec is attach-or-start. Calling it again with the same id attaches to the running or finished command instead of spawning a second one.
Now a DBOS step that runs a command can safely re-run as a whole after any restart: the replay attaches to the original exec and gets the original result.
We used this same pattern for every side effect — because we can lean on DBOS for at-least-once delivery, with idempotency for every operation, we can guarantee effectively-once delivery for every automation:
| Side effect | Deterministic ID | Deduped by |
|---|---|---|
| Run a command in the VM | A deterministic exec id | The VM’s exec journal: attach or start |
| Send a prompt to the agent | An id from the triggering event, such as slack:<event_id> | The harness in the VM, which remembers prompt ids across restarts and snapshots |
| Start a run from a webhook | autorun:<automationId>:<provider>:<deliveryId> as the workflow id | DBOS |
| A cron occurrence | The occurrence epoch in the run id | DBOS |
Results
engrams is a software factory that “just works” only because of the durable workflow model that DBOS gives us. A block graph someone drew in an editor gets the same guarantees as code we wrote ourselves: it survives restarts and deploys, it can wait days for a reply, and every side effect, down to a command inside a VM, happens once. And it runs in the Postgres every engrams install already has.
If you’re building agents that act over hours or days, try engrams, or start with DBOS in your own Postgres!