Six properties that decide everything else
Every verdict on this page traces back to one of these. Read them once and the rest of the page is derivable.
- 1 · runtime graph The agent decides the next step, so you cannot commit control flow at design time. Deciding at runtime is the whole reason you added a model.
- 2 · unbounded step A step takes seconds or minutes, and may retry. Nothing can hold a connection, a lock, or a lease across it.
- 3 · no undo The step posted a comment, pushed a branch, spent tokens. There is no rollback, only compensation.
- 4 · nondeterminism The same input twice gives different bytes. Anything that assumes two replicas compute the same answer fails here.
- 5 · state outlives compute Runs are long and processes die. The job record cannot live in the memory of whatever is currently working on it.
- 6 · a human is inside Review is a participant, not an exception. A run can wait hours at a checkpoint and that is the design working.
None of those six is exotic on its own. Long-running jobs, at-least-once delivery and human approval steps all predate agents by decades. What is unusual is getting all six at once, in every workload, with no way to opt out of any of them.
What this page does not do
- It does not benchmark anything. There are no latency or throughput numbers here, because I did not collect any.
- It does not argue that iii is the right substrate. Four of the five models below are older than any agent runtime and port to other queues.
- It does not cover the models in depth. Three of them have their own pages, linked in place.
- It does not claim the rejected list is complete. It is the five I keep seeing reached for.
The counter-argument
You can build an agentic system as one process with a loop and a list of tools. For a single-user prototype that is the correct answer, and nothing here beats it on time to first result. The shapes below start paying later: when a run outlives a process, when two runs overlap, or when someone has to explain what the system decided and why.
A reliable queue, before anything else
The durable handoff is not one of the five models so much as the floor the other four stand on. It answers properties 2 and 5 directly.
A queue moves the job record out of the worker. State lives in the message and in whatever store the message points at, so the process holding it becomes disposable. Kill a worker mid-step and the work is still there. That single property is what makes a forty-minute run survivable on hardware you restart.
It also gives you a place to put a step that has not happened yet. A synchronous caller has to wait or give up, while a publisher writes a message and stops caring. When a human checkpoint can sit for hours, per property 6, having somewhere to park the job is the difference between a design and a timeout.
The bill
Delivery is at-least-once, which means every consumer has to be safe to run twice. On an ordinary service that is a nuisance. With agents it is a real hazard, because property 3 says the duplicate already posted the comment. Make the write idempotent, key it on something stable from the message, and treat the notification as a doorbell rather than as data.
The second charge is subtler. You will want a lock somewhere, and property 2 says you cannot have one. A lease long enough to cover a slow model call is a lease long enough to strand work when a node dies. Optimistic writes with an explicit parent chain handle this better than any lock length I have tried.
Nanoservices, and why the transport is the point
Microservices decompose at the service level. Nanoservices decompose at the function level. That distinction is not mine and it is not new. What changes on iii is the transport.
The term, and what it does not settle
A microservice does one job as a service. A nanoservice is abstracted a level down, at the function. I have gone back and forth on the word and I am settling on it. "Very small microservice" describes the size while missing the thing that actually moved, which is the unit of abstraction.
Function-level decomposition is also what functions as a service already means, so granularity alone does not separate iii from Lambda. Both put one function behind one address. The difference is what that address is.
| Microservices | FaaS | Nanoservices on iii | |
|---|---|---|---|
| Unit | a service | a function | a function |
| Invoked by | a web request | a web request | a function call |
| Carried over | HTTP per call | HTTP per call | a websocket held open to the engine |
| Addressed as | a host and route | a cloud endpoint | a queue topic it subscribed to |
| Needs | servers, ingress, routing | a cloud account | the engine, and a process |
| Integration | a versioned contract between deployments | a versioned contract between deployments | one definition, changed in one commit |
Why that transport suits agents
The network traffic happens inside the engine. A node connects out, holds a websocket, and receives a function call, so there is no web request between the pieces of your system. No DNS lookup, no connection setup, no HTTP framing, and no ingress hop per call.
That matters more here than it would on a service graph, and the reason is granularity. Cut a system at the function level and you get many small hops where a microservice graph had a few coarse ones. Per-hop overhead becomes the dominant overhead. So the cheaper hop is what makes the finer cut affordable at all, which I think is why FaaS graphs tend to stay coarse in practice.
Two smaller consequences follow. There is no HTTP surface per node, so nothing to route, authenticate at the edge, or expose. And a node is a process holding a socket rather than a server. The same graph runs on a laptop in a few terminals, or in a cluster, without changing shape.
Why it fits property 1
A node registers its functions, subscribes to a topic, and knows nothing about its neighbors. Nothing in the code names the next step. The graph lives in configuration instead, which is what lets the wiring change without editing a caller.
Choreographed services are the contrast worth drawing. When each service calls the next by name, the code holds the graph. But property 1 says that graph is the part an agent decides at runtime. So a shape that hard-codes the order fights the one thing you added the model to do.
The integration tax it does not charge
The network overhead is the smaller saving. The larger one is that a lot of microservice pain is not runtime at all: it is keeping independently deployed services in sync. Two services agree on a contract and one ships a change. Now you are versioning an API, coordinating a release order, and writing tests that exist only to prove the pieces still fit together.
Here the whole graph is defined in code, in one place, and the engine runs it. A change that updates a function updates its callers too, or the engine fails. That is a guarantee, not a convention, and it removes the coordination cost. There is one definition and one engine reading it, not a contract negotiated between two deployments.
Compare where the two designs put the mismatch. Independently deployed services defer it to runtime, in production, as a 500 from a caller built against last week's shape. The engine refuses instead, at the point you wired it wrong, which is the cheapest moment to find out.
Be clear about what that trades away. Microservices buy independent deployability, and this gives it up: one definition means one blast radius, and every node shares the engine version. If your problem is many teams shipping on separate clocks, that is the wrong trade. This shape suits a small team, or one engineer with an agent.
The bill
The engine becomes a dependency and a single point of failure. You also give up the HTTP
tooling, so gateways, service meshes, ordinary tracing and a quick curl all
stop applying. And the graph is now only as legible as its configuration, which means a
configuration nobody can read is a system nobody can read.
The blackboard, for work with no fixed order
Hearsay-II solved speech understanding in 1975 with a shape that has no control flow at all. Nothing calls anything, which is why it answers property 1 better than a pipeline does.
Contributions land in a shared knowledge space and nowhere else. Each specialist declares an activation condition, which is a predicate over that space deciding whether it is worth waking. Add a specialist and you add a predicate, not an edge, so the order of operations emerges from what is currently known.
Agents already behave that way. A pipeline commits to the order at design time, so every time the work needs a different order, you add a branch or a retry loop. The blackboard never had an order to commit to.
The bill
Control moves from a nonexistent problem to the central one. With plain predicates, choosing which eligible specialist runs next is an optimization. With a model deciding, it is also a cost question, because every scheduling decision can be an inference call you pay for. Budget that explicitly or the architecture bills you by the loop.
The Blackboard Architecture for Agents is the long version, built twice, with the activation condition as the load-bearing part.
Actors and supervision, mostly as configuration
If you have written Elixir you carry a supervision tree in your head. Most of it ports to a durable-queue system as pure configuration. The parts that dissolve are the interesting ones.
OTP assumes a process holds state, dies, and gets restarted. A queue system models the same territory differently, because state outlives the thing computing it, per property 5. So failure belongs to a message rather than to a process, and restart intensity becomes a retry policy on a topic.
Half of the vocabulary survives that translation as a YAML entry or a trigger binding. The other half has no referent, and not because a feature is missing: the problem it solved does not arise once state is durable. That is the finding I did not expect when I built it both ways.
The bill
Supervision measures the wrong thing on its own. In the OTP build the supervisor worked perfectly while the work never finished. Watch for that failure mode: every process healthy, every restart correct, and no progress. Supervise liveness and completion separately, because the first tells you nothing about the second.
Actors and Supervision for Agents has the concept-by-concept mapping, including the genuine gaps that no configuration closes.
Event sourcing, which you are doing already
An agent produces an append-only log whether or not it means to. The choice is not whether to have one, but what you get out of it.
Event sourcing derives state instead of storing it. The log is authoritative and every read model is a fold over it, disposable and rebuildable. Your transcript is already that log, which is why this is the cheapest of the five to adopt. Commit to what you have and you get replay, projections and an audit trail.
It answers three properties at once. Property 3 says you cannot undo a step, so a record of what was decided is the only accountability available. Property 4 says the run is not reproducible, which makes the log the sole evidence of what actually happened. And property 6 gives you a reviewer who needs to read that evidence months later.
The bill
The hazard is quiet. Two writers append against the same parent, both get a success response, and one event survives. Nothing anywhere reports a problem. An explicit parent chain plus a rejected write is the fix. Care about it now, because this failure never shows up in a log you are only grepping.
Event Sourcing for Agents walks the hazard end to end, with notification treated as a doorbell rather than as data.
Five shapes that do not fit, and the assumption each one makes
None of these is a bad design. Each is a good design for work that is deterministic, bounded in time, or undoable, and agent work is none of the three.
- synchronous RPC chains Violates 2 and 5. Every hop holds a connection while a model thinks, and the deepest timeout governs the whole chain. Publish and poll instead, which is the shape the iii harness uses end to end.
- distributed transactions Violates 3, and 2 on the way past. Two-phase commit needs participants to hold locks until a coordinator decides, and the branch your step already pushed cannot be rolled back anyway. Use compensation, and make each step idempotent.
- consensus as the workflow Violates 4. State machine replication assumes replicas fed the same input reach the same state, and two model calls do not. Consensus underneath you is fine and often necessary; consensus as the thing running your steps is not.
- a shared mutable database Violates 3, and throws away the audit trail. When nodes coordinate by updating the same rows, two agent writers race and the loser's decision disappears with no record it existed. §5 is the alternative.
- fan-out with a barrier Violates 1 and 2. The bulk-synchronous shape needs the task count up front and all of them to finish, while agents discover work at runtime and partially fail. Let each result land as its own message.
Read down the reasons and the pattern is one assumption, worn five ways. Each shape needs the work to be predictable in the exact dimension where an agent is not. No amount of configuration converts an assumption into a fact.
Where I could be wrong
This list is reasoning from §0, not a report on five failed builds. I did not construct each rejected shape to watch it break, so treat the verdicts as predictions with their mechanism stated, which at least makes them arguable. If you have run one of these against real agent traffic and it held, the mechanism above is the part to attack.
One boundary is worth naming. Everything here assumes the model is in the loop at runtime. Batch inference over a fixed set of inputs is deterministic enough at the system level, so several of these shapes come back. MapReduce over a corpus of documents is a perfectly good design.
Choosing, and what I still do not know
You do not pick one. The queue is the floor, nanoservices are how you cut the work, and the other three answer questions you may not have yet.
| When this is your problem | Reach for |
|---|---|
| A run outlives the process running it | a reliable queue · §1 |
| You want to rewire the graph without editing callers | nanoservices · §2 |
| The order of work is not knowable in advance | the blackboard · §3 |
| Failure and restart policy is the pain | actors and supervision · §4 |
| Someone will ask what the system decided, and why | event sourcing · §5 |
Take the smallest workflow you already run as one process with a loop. Cut it at the function level, put a durable queue between the pieces, and keep the transcript as the authoritative log. Those are §1, §2 and §5, and together they are a weekend rather than a quarter. Ghola is the configured starting point if you would rather not wire it yourself.
The open question I cannot answer yet: where the function-level cut stops paying. Fine granularity buys rewiring and costs legibility, so somewhere there is a node count at which the configuration becomes the system nobody can read. I have not found that number. I am not sure it is a number at all, rather than a property of how someone wrote the configuration.