← Dallen Pyrah

The Resident Model

How I think about agents that live in the database, not in memory

The premise

An agent spends almost all of its life waiting.

It waits for a person to reply. It waits for a tool to finish. It waits for an approval, a webhook, a scheduled follow-up, another agent. The moments of actual compute, when a model is producing tokens and tools are running, are thin slices between long stretches of nothing.

Now look at how we usually build stateful, long-lived things in software: the actor model. An actor is an object in memory with a mailbox. It is alive. It holds its state in RAM, processes one message at a time, and exists exactly as long as some process holds it.

That shape fights the agent workload at every step.

Keeping a process alive per agent is wasteful when the agent is 99% idle. A deploy kills every actor mid-thought. A crash loses every mailbox that was not separately persisted. So we bolt durability on: event sourcing, snapshots, persistence plugins, replay machinery. The durability is an accessory, and everything the accessory does not cover is lost.

I kept asking a simpler question while building Relay: what if the durability were not the accessory?

What if it were the actor?

The inversion

Here is the whole idea in one sentence.

A classic actor is a process that owns its state. A resident is state that occasionally borrows a process.

A resident is a database record. Its identity is a row. Its mailbox is a table. Its state is a table. Its position in execution is a row that says what it is waiting for. There is no long-lived process anywhere.

Compute is transient and fungible. When a message arrives, a worker, any worker, wakes up, loads the resident from the database, runs one turn, commits the results back to the database, and walks away. The process is disposable. The record is what lives.

process-first actoractor in memorystate · mailbox · lifepersistence (optional)residentrecord in the databasetransient workerload · one turn · commitloadcommit
The inversion. On the left, the process is the actor and the database is an accessory. On the right, the record is the actor and the process is an accessory.

The analogy I use is a phone call versus an email account. A phone call only exists while both processes are alive. An email account exists whether or not you are logged in. Mail accumulates while you are away, and you process it when you next sit down.

A resident is the account, not the call.

This is not event sourcing in disguise, and your agent code is not written as replay-deterministic workflow code. The state is stored, not derived. A worker loads the row instead of replaying the resident's history through your code the way an entity workflow does. Relay does use workflow replay internally to make a single turn's side effects crash-safe, but that machinery never leaks into how you write the agent or how long-lived state is recovered.

The row simply says where you are.

Anatomy of a resident

A resident has four parts, and every one of them is rows.

An address. A resident is reached by kind and key: chat-room plus team-42. Asking for a resident is the same operation as creating it. There is no spawn-then-hold-a-reference step, because there is nothing to hold. The address is deterministic, so any process that knows the kind and key can reach the same resident.

An inbox. Sending a message means inserting a row. That insert is transactional and durable before anyone has looked at it. Nothing needs to be listening for the send to succeed, and nothing is lost because a process was down.

State. A small keyed store scoped to the resident. A state change commits in the same transaction as the event that records it.

A wait. Between messages the resident is parked on a durable wait. Not blocked in memory. Parked: a row that says, wake me when mail arrives.

In Relay, defining and reaching a resident looks like this. The SDK is Effect-native, so the client is a service and every failure is typed.

Define a resident kind and open one
import { Client, Content, Ids } from "@relayfx/sdk"

const roomKind = Ids.EntityKindName.make("chat-room")

// Register the kind once: which agent runs its turns.
const registerRoom = Client.registerEntityKind({
  kind: roomKind,
  agent_id: Ids.AgentId.make("agent:room"),
})

// Reaching a resident is the same operation as creating it.
const openRoom = (key: string, createdAt: number) =>
  Client.getOrCreateEntity({
    kind: roomKind,
    key: Ids.EntityKey.make(key),
    created_at: createdAt,
  })

// Sending is inserting a durable row. Nobody has to be listening.
const post = (to: Ids.AddressId, text: string) =>
  Client.send({
    from: Ids.AddressId.make("address:you"),
    to,
    content: [Content.text(text)],
    idempotency_key: `post:${to}:${text}`,
  })

A note on names: Relay's code currently says Entity in several places where this essay says resident. The vocabulary is converging on resident. The model is the same either way.

For request and response, ask sends an envelope with wait semantics: it delivers the message, parks a wait for the reply, and resolves with a decoded, schema-typed answer or a typed timeout.

One turn, then commit

The unit of resident compute is a turn.

A message arrives. The runtime wakes a worker. The worker loads the resident's state and history and runs the turn. The new state commits in the same database transaction as the event that records the change. Outgoing messages ride a transactional outbox: the message row and its delivery row commit together, and delivery happens afterward. Every write carries an idempotency key.

sendinsert into inboxwakeany workerrun one turnload state · thinkcommitstate + event: one txnenvelopes: durable outboxpark again on a durable wait
One turn. The state change and its event commit atomically; outgoing messages ride a transactional outbox; a crash before a commit is retried, never half-applied.

Those commits are where the crash-safety comes from.

Die before a commit, and that write never happened. The message is still in the inbox, it is delivered again, and the turn reruns. The idempotency keys make the rerun land each write exactly once. There is no half-updated resident, because half was never visible to anyone.

Die after the commit, and the work is simply done. The next worker to touch the resident sees the new state, because the new state is the row.

Replay-based systems get this property from determinism constraints and event-history machinery. Here it comes from the oldest tools in the building: the database transaction, the outbox, and the idempotency key.

Sleeping is free

A parked resident costs nothing.

No goroutine, no fiber, no heartbeat, no connection. It is rows. A million sleeping residents is a storage bill, not an operations problem.

That changes what you can afford to model as an actor. A conversation that spans weeks. A subscription that wakes monthly. A research task that waits days for a human to approve step four. In a process-first system, each of those is a liability you must keep alive or carefully reconstruct. In a record-first system they are just data that occasionally computes.

A deploy stops being dangerous. Killing every process in the system loses nothing: workers restart, look at the durable waits, and pick up where the rows say to pick up.

Long-lived residents do accumulate history, so the runtime rolls a resident over after a configured number of turns. The identity, inbox, and log stay continuous. Rollover is bookkeeping, not a new actor.

The database is the actor system

Because a resident is rows, everything you already know how to do with rows now works on your actors.

You can SELECT them. How many rooms are active? Which residents have been waiting longest? Which ones are stuck? That is a query, not a custom introspection API.

You can migrate them. Actor state schema changes are database migrations, a problem your team already has tools, review habits, and rollback procedures for.

You can back them up, restore them, and join them against your product tables, because they live in the same database your product already operates. Relay deliberately does not host anything: you bring Postgres, MySQL, or SQLite, and the runtime embeds in your application.

your databaseresidentkind · keyinboxdurable mailstatekeyed rowseventswhat happenedwaitswake me
A resident is five kinds of rows. The schema is the actor model.

There is an honest consequence hiding in that figure: if the row is the actor, then the schema is the actor model. The quality of the tables is the quality of the abstraction. That is a constraint I like, because it means the design work happens in a medium with fifty years of accumulated discipline.

If you squint, none of the ingredients are new. Stateless workers over a database, a transactional inbox and outbox, jobs claimed from a table: operations teams have assembled ad-hoc versions of this against Postgres for decades.8 The resident model takes that same substrate and gives it an actor-shaped front door instead of a job-queue-shaped one.

Why agents need this

I said at the start that agents are mostly waiting. The second half of the argument is what a resident's turn actually runs.

In Relay, the turn is an agent turn: Baton, the Effect-native agent loop, runs the model call, executes typed tools, and reports what happened. Baton on its own is deliberately not durable. It is ordinary software: an agent is a value, its dependencies are services, its seams have deterministic test implementations. Relay wraps that loop in durability without changing what it is.

The two models compose because waiting is first-class.

When an agent needs a human approval, that is not an awkward pause in a running process. The turn ends, the approval becomes a durable wait, and the resident sleeps. The approval arriving next week is just mail. Human-in-the-loop stops being an architectural problem and becomes the normal case of the model.

Ask a resident and wait for a typed reply
import { Client, Command, Ids } from "@relayfx/sdk"
import { Schema } from "effect"

// roomKind comes from the earlier listing.
const Summarize = Command.make("summarize", {
  input: Schema.Struct({ topic: Schema.NonEmptyString }),
  output: Schema.Struct({ summary: Schema.String }),
})

// Delivers an envelope, parks a wait for the reply, resolves typed.
const summary = Client.askEntity({
  kind: roomKind,
  key: Ids.EntityKey.make("team-42"),
  command: Summarize,
  input: { topic: "this week" },
  from: Ids.AddressId.make("address:you"),
  timeout: "30 seconds",
})

Multi-agent systems fall out the same way. A resident spawning a child run, an agent asking another agent, a fan-out across a dozen workers: each edge is a durable message to an address, and each participant can crash, deploy, and resume without the conversation noticing.

The nearest neighbors

The resident model is a synthesis, and I want to be precise about what it borrows and where it differs.

Nearest neighbors, and the divergence
SystemWhat matchesWhere it diverges
Orleans virtual actors3Actors always exist; addressing by type and key; activation on demand.Memory-first. The activation in RAM is the actor; persistence is an optional provider behind it.
Temporal entity workflows4Long-lived identity receiving signals; continue-as-new rollover.State is derived by replaying event history through deterministic code. Opaque to SQL, and the determinism constraints reach into your own code.
Restate virtual objects5Keyed durable handlers; single writer per key; exactly-once invocations.Restate owns the log and the store. Your database is not the truth; theirs is.
Cloudflare Durable Objects6Id-from-name addressing; objects hibernate with state in storage.Platform-locked, and still process-attached: the in-memory object is the programming model.
DBOS7Your Postgres is the runtime; steps are transactions; no separate infrastructure.Workflow-shaped, not actor-shaped: no addressable identity, no mailbox, no ask.

The genealogy, roughly: Orleans' addressing, crossed with Temporal's lifecycle, on DBOS's substrate, running agent-native turns.

What sets a resident apart is the combination this essay opened with: an addressable, mailbox-carrying actor that is also relational data in your own database, queryable and migratable and joinable with your product tables.

Where this breaks

The model has real costs, and they should be stated plainly.

Every turn pays a database round trip. This is the wrong model for microsecond actor messaging. If you are building a game server tick loop or an in-memory trading system, a resident is the wrong tool. The model is for work where durability, addressability, and inspectability matter more than latency. That describes the agent workload, and rarely the hot path.

Hot residents serialize. One resident processes one turn at a time, and its throughput is bounded by the database. A resident that receives thousands of messages per second is a design smell in this model; the answer is usually more residents with smaller scopes, not a faster row.

You give up time travel.Temporal's replay determinism is a real strength: it can re-derive any historical state and step through a workflow's past. Storing state instead of replaying history trades that away. The event log still records what happened, but the code is free, and freedom cuts both ways.

The commit points are the whole promise. Everything I said about crash-safety concentrates into a small number of transactions and idempotency keys. If an implementation commits a state change and its event separately, or forgets a key on a retried write, the model's central claim silently breaks. This is where I point adversarial testing before anything else: kill the process between any two writes and see what the rows say.

The abstraction is young. Relay is pre-1.0, the vocabulary is still converging, and the first product built on it taught us a long list of places where the interfaces need to be better. I consider that normal. The substrate is old; the front door is new.

The record outlives the process

Most of the systems I admire share one property: the important truth lives somewhere that survives the people and processes that produced it.

The actor model got the programming shape right. Isolated state, message passing, one thing at a time. What it got wrong, for this era, is where the truth lives. It lives in a process, and processes are the least durable thing we operate.

Agents force the issue. They live for weeks, wait for humans, survive deploys, and coordinate with each other. They need the actor model's shape with a database's permanence.

The resident model is that combination.

Not a process that persists. A record that lives, and a process that visits.

References

  1. Hewitt, Bishop & Steiger (1973). “A Universal Modular ACTOR Formalism for Artificial Intelligence.”
  2. Armstrong (2003). “Making reliable distributed systems in the presence of software errors.”
  3. Bernstein, Bykov, Geller, Kliot & Thelin (2014). “Orleans: Distributed Virtual Actors for Programmability and Scalability.”
  4. Temporal. “Temporal Workflows,” including continue-as-new and the entity workflow pattern.
  5. Restate. “Services, Virtual Objects, and Workflows.”
  6. Cloudflare. “Durable Objects.”
  7. Skiadopoulos et al. (2022). “DBOS: A DBMS-oriented Operating System.”
  8. Richardson. “Pattern: Transactional outbox.”
  9. Helland (2007). “Life beyond Distributed Transactions: an Apostate's Opinion.”
  10. RelayFX documentation and BatonFX documentation.