Academy

Event-driven by design: agent teams that don't lose messages

When several agents work together, the biggest risk is lost messages and a collapsing chain. An event-driven architecture removes that risk with a few simple rules.

The Felesh team 6 min

When a single agent works, everything runs along one path and coordination error has no meaning. But the moment several agents are involved, a new risk appears: messages get lost, two agents work on the same thing at once, or the chain collapses mid-task. An event-driven architecture is an answer to that risk. Let’s see how.

Events instead of direct calls

In an event-driven architecture, agents do not call each other directly. Instead, when an agent finishes its work, it publishes an event — simply stating “this task is complete” — and any component listening for that event reacts. This decoupling means no agent needs to know what comes next; it simply announces that its work is done.

Publishing an event instead of calling an agentThe research agent finishes task t-318 and publishes the event done t-318 to the event bus, which already holds the events for t-316 and t-317. The bus hands it to three subscribers: the draft agent starts work, the monitor reads the board, the audit log records it. The research agent knows none of them.publishersubscribersresearcht-318 finishedknows no oneevent busdone · t-316done · t-317done · t-318draft agentstarts workmonitorreads boardaudit logrecords itit announces; whoever listens reacts
The research agent publishes one event and moves on; the draft agent, the monitor and the audit log each react on their own.

The event is the trigger, memory is the truth

A fundamental rule is the distinction between a trigger and the truth. An event is a notification; it should not carry heavy data payloads. It carries only an identifier — such as a task ID — and any component that reacts reads the up-to-date state from shared memory, not from the event itself. If you put state data in the event, that data may be stale by the time it is processed. Memory remains the single source of truth; the event merely says “something changed, go look.”

The event is the trigger, memory is the truthAn event arrives carrying only an id, task.changed t-318. The monitor reads memory, where the row for t-318 now says done, replacing the running it had earlier. A status copied into the event would still say running, which is stale on arrival. The event says go look; memory is the answer.triggertrutheventtask.changedid: t-318monitormemoryt-317donet-318activedonet-319queuedif the event carried the status:status: runningstale by the time it arrivesthe event says go look; memory answers
The event carries only the id; the monitor reads the current status from memory, because a status copied into the event is already stale when it arrives.

At-least-once delivery

To ensure messages are never lost, events must be durable. The standard pattern is at-least-once delivery: the event is stored in a durable queue, and the consumer acknowledges it only after successful processing. If processing fails and no acknowledgement arrives, the event is delivered again. No message is silently dropped. Events that repeatedly fail to process go to a dead-letter queue for investigation, rather than vanishing in silence.

At-least-once delivery, step by stepThe durable queue delivers event t-318 to the consumer. The consumer crashes before finishing and sends no ack, so t-318 stays in the queue. The queue delivers t-318 again; this time the consumer finishes and sends an ack, and t-318 is removed. Below, the dead-letter queue holds t-290, which failed 3 times and is held for review rather than dropped.durable queueconsumerdeliver t-318crashes before finishingno ack sentno ack: t-318 stays queueddeliver t-318 againack, after it succeedst-318 removed from the queuedead-letter queuet-290 failed 3 times: held for review, not dropped
A delivery counts only once the consumer acknowledges it: no ack means the event comes again, and an event that keeps failing is parked in the dead-letter queue.

The completion monitor: the deterministic decider

At the heart of this architecture sits a specific role: the completion monitor. This component listens for all “task complete” events, reads task statuses from shared memory, and decides the next step: whether another task should begin or the system should return the final result. The critical point is that this decision-making is deterministic — no language model is involved. Because decisions rely on fresh state and explicit rules, race conditions and dropped messages disappear.

The completion monitor decides by ruleThe draft agent finishes t-2 and the event done t-2 reaches the monitor. The monitor reads the board in memory: t-1 research is done, t-2 draft is done, t-3 review is waiting, t-4 send reply is waiting. Its first rule, t-3 after t-2: start, fires, and t-3 changes from waiting to running. The other rules, t-4 after t-3: wait and all done: return, do not fire. No language model is involved: the same board and rules always give the same next step.draft agent finished t-2task.done · t-2board in memoryt-1 researchdonet-2 draftdonet-3 reviewwaitingrunningt-4 send replywaitingmonitorreads board, then:t-3 after t-2: startt-4 after t-3: waitall done: returnno model, only rulessame board, same rules, same next step: no model in the loop
Each done event makes the monitor read the board and apply fixed rules, so the same board always produces the same next step.

How this compares to other ways to compose

Event-driven coordination is not the only way to compose a multi-agent system; it is one of a small family of patterns, and it helps to see where it sits. In an orchestrator pattern, one central agent decides who runs and in what order. This is simple to trace, but the orchestrator becomes a bottleneck that everything waits on. In a pipeline, work flows through agents in a fixed sequence, each transforming the previous output. This is predictable but rigid, as every stage is locked to the data shape of the stage before it. In a mesh, agents are peers that call each other freely. It is highly flexible, but coordination becomes implicit and difficult to trace when something goes wrong.

The event-driven approach retains the flexibility of a mesh — no agent is hardwired to the next — while avoiding its primary weakness. The trail of what happened and what comes next resides in two explicit, inspectable places: the durable event log and the deterministic completion monitor. You trade the at-a-glance simplicity of a fixed pipeline for a system that absorbs load and complexity without losing messages. As with most architectural choices, the pattern follows the problem: reach for events when the components are numerous, independent, and likely to change. For a single straight line of work, a plain pipeline remains the simpler answer.

Four ways to compose agentsA table of four patterns. Orchestrator: one central agent picks who runs; the catch is a bottleneck that everything waits on. Pipeline: a fixed sequence where each stage feeds the next; the catch is rigidity, each stage locked to the one before. Mesh: peers call each other freely; the catch is hidden coordination that is hard to trace. Event-driven: a monitor decides by rule from memory; the trail is inspectable, though less simple at a glance.patternwho picks nextthe catchorchestratorpipelinemeshevent-drivenone central agentpicks who runsa fixed sequence,each feeds nextpeers call eachother freelymonitor decidesreading memorya bottleneck thateverything waits onrigid: each stage islocked to the lasthidden coordination,hard to tracetraceable, but lesssimple at a glance
The orchestrator, the pipeline and the mesh each fail in their own way; the event-driven design keeps the mesh's freedom and moves the trail into a log and a monitor you can inspect.

Why this design doesn’t collapse

The value of this architecture lies in its clean separation of responsibilities. Events are simple notifications (identifiers, not data payloads). Memory is the single source of truth (no parallel state scattered across the system). The deciding roles are deterministic (not probabilistic). Because each component has a tightly scoped responsibility, a failure in one does not cascade to the others. This is what turns a multi-agent team from a fragile chain into a stable system — one that doesn’t lose messages even under pressure.