AI Engineering
tutorial
Featured

Your Multi-Agent System Already Has a Blackboard

Wire a few agents together with direct handoffs and it works. Add a fifth and the wiring becomes the system, brittle and impossible to trace. Most teams drift into a shared context blob that nobody designed, then spend weeks debugging it. That blob is a blackboard, a forty-year-old architecture pattern, and building it on purpose instead of by accident is what keeps a multi-agent system auditable as it grows.

Viral Ruparel
10 min read
Share:

You built a multi-agent system the obvious way. A planner hands off to a retriever, the retriever hands off to a writer, the writer hands off to a critic. Direct calls, clean and readable. Then the requirements grew. The critic needed to see what the planner intended, not just the draft, so you passed the plan through. The retriever started needing the critic's last round of feedback to search better. A fact-checker went in between the writer and the critic. Now every agent's function signature takes half the other agents' outputs, and adding the seventh agent means touching four existing ones. The wiring has quietly become the system, and nobody can hold the whole graph in their head anymore.

This is the failure mode that shows up around the fifth agent, and it is worth naming precisely because the fix is not more discipline in how you pass arguments. The fix is to stop passing arguments.

The mesh is the problem, not the symptom

Direct handoffs are point-to-point coupling. With N agents that all need to see each other's work, you are heading toward N-squared connections, and each one is a place where an interface change ripples outward. Worse, the flow of control and the flow of data are tangled together. Agent B runs because agent A called it, so the order agents run in is hardcoded into the call graph. The day you want the critic to run before the fact-checker on some inputs and after it on others, you are rewriting control flow, not configuration.

Most teams feel this and reach for the same escape hatch without naming it. They introduce a shared state dict, a context object, a blob that every agent reads from and writes to. Now agents do not call each other, they just read what they need off the shared object and drop their results back on it. The mesh disappears. This works, and it keeps working right up until the blob has thirty keys, three agents write to results and you cannot tell which, and a value that looks current is actually two rounds stale because the agent that produced it ran early and never got asked again.

That shared blob is a blackboard. You built one by accident. The reason it turned into a mess is not the pattern, it is that you implemented a quarter of it. This is the same trap I described in why stale tool observations quietly poison an agent's context: the state is there, but nothing governs its freshness or its provenance, so it rots in place.

What the pattern actually is

The blackboard architecture is one of the oldest ideas in AI systems, from speech-understanding work in the 1970s, and it has three parts. There is the blackboard itself, a shared, structured store of partial results. There are knowledge sources, the specialists that each know how to contribute one kind of thing, which in a modern system are your agents. And there is a control component, the piece that looks at the current state of the board and decides which knowledge source gets to run next.

The accidental version has the first part and skips the other two. Every agent is a knowledge source, sure, but they all run on a fixed schedule you wrote by hand, and the board is an untyped dict. Building it deliberately means making the entries structured and traceable, and making the control component a real thing rather than a for-loop over your agent list.

Start with the board. The important move is not that it is a store, it is that every entry carries who wrote it, when, and what it was derived from.

# blackboard.py -- one shared store every agent reads and writes.
# The point is not the list, it is that every entry records its author,
# its parents, and what it supersedes, so the state stays auditable
# long after the run finishes.
import time
import itertools
from dataclasses import dataclass, field
from typing import Any

_ids = itertools.count()

@dataclass
class Entry:
    kind: str                          # "question", "retrieval", "draft", ...
    value: Any
    author: str                        # which agent wrote it
    derived_from: list[int] = field(default_factory=list)  # parent entry ids
    supersedes: list[int] = field(default_factory=list)    # entries this replaces
    id: int = field(default_factory=lambda: next(_ids))
    ts: float = field(default_factory=time.time)

class Blackboard:
    def __init__(self) -> None:
        self._entries: list[Entry] = []

    def write(self, entry: Entry) -> Entry:
        self._entries.append(entry)
        return entry

    def read(self, kind: str | None = None) -> list[Entry]:
        rows = self._entries
        return [e for e in rows if kind is None or e.kind == kind]

    def latest(self, kind: str) -> Entry | None:
        rows = self.read(kind)
        return rows[-1] if rows else None

Nothing here is clever. The discipline is that agents write Entry objects and never touch each other's, and that derived_from and supersedes are filled in honestly. That single constraint is what buys you a system you can debug six weeks later, because you can trace any decision back through its parents to the raw inputs.

Agents as knowledge sources that watch the board

A knowledge source has two responsibilities: know when it has something to contribute, and contribute it. Splitting those apart is what lets the control component schedule work without hardcoding the order.

# knowledge_source.py -- an agent that watches the board for work it can do.
# trigger() answers "given the current state, do I have anything to add?"
# contribute() reads only the entries it needs, does its work, and writes
# the result back with provenance so the chain stays traceable.
from dataclasses import dataclass
from typing import Callable

@dataclass
class KnowledgeSource:
    name: str
    trigger: Callable[[Blackboard], bool]
    contribute: Callable[[Blackboard], Entry | None]

def retriever_ks(call_search) -> KnowledgeSource:
    def trigger(bb: Blackboard) -> bool:
        # Fire when there is a question but no retrieval for the latest one yet.
        q = bb.latest("question")
        if q is None:
            return False
        return not any(q.id in e.derived_from for e in bb.read("retrieval"))

    def contribute(bb: Blackboard) -> Entry | None:
        q = bb.latest("question")
        docs = call_search(q.value)
        return Entry(kind="retrieval", value=docs, author="retriever",
                     derived_from=[q.id])

    return KnowledgeSource("retriever", trigger, contribute)

The trigger is the whole game. This retriever fires exactly once per question, because once a retrieval entry exists that was derived from the current question, the trigger goes quiet. Get this wrong, write a trigger that stays true after the agent contributes, and you have an infinite loop where the same agent runs forever. Triggers must consume their own reason for firing.

Notice also that contribute reads only question, not the whole board. That scoping is what keeps token cost down. An agent that pulls the entire blackboard into its prompt on every turn is how the "blackboards are expensive" reputation gets earned. Subscribe to the kinds you need and nothing else.

The control loop is the part you are missing

Here is the piece the accidental version skips entirely. Something has to decide which agent runs next, and running all of them on every tick is not that decision, it is the absence of one.

# control.py -- pick the highest-priority ready source, run it, repeat until
# the board goes quiet or we burn the step budget. Priority is just list
# order here; a real system can score readiness or let an LLM pick.
def run_blackboard(bb: Blackboard, sources: list[KnowledgeSource],
                   *, max_steps: int = 50) -> Blackboard:
    for _ in range(max_steps):
        ready = [ks for ks in sources if ks.trigger(bb)]
        if not ready:
            break                      # nobody has anything to add, we are done
        ks = ready[0]                  # sources listed in priority order
        entry = ks.contribute(bb)
        if entry is not None:
            bb.write(entry)
    return bb

This is thirteen lines and it replaces your hand-wired call graph. Adding an eighth agent now means appending one KnowledgeSource to the list, not editing seven functions. The order agents run in is emergent from their triggers and the state, so the same code handles "critic before fact-checker" and the reverse without a branch. The max_steps budget is your seatbelt against a trigger bug turning into a runaway loop, and it belongs there even once you trust your triggers.

The control component is also the natural home for anything smarter than round-robin. You can score ready sources by expected value, run several non-conflicting ones in parallel, or hand the current board to a cheap model and let it pick the next agent. That last option is where this connects to orchestrator-style designs; the orchestrator-worker architecture I walked through here is essentially a blackboard whose control component is itself an LLM deciding who works next.

Staleness, races, and the audit trail

The reason to record supersedes shows up the moment an agent revises something. An agent never mutates an existing entry, because mutation destroys history and creates races when two agents touch the same key. It writes a new entry that points back at the one it replaces, and readers ask for a live view that filters superseded parents out.

# live.py -- the current view of the board, with replaced entries hidden.
# The superseded entries stay in the store as the audit trail; only the
# reader-facing view drops them, so context windows see current state
# while post-mortems see the whole chain.
def live(bb: Blackboard, kind: str | None = None) -> list[Entry]:
    replaced: set[int] = set()
    for e in bb.read():
        replaced.update(e.supersedes)
    return [e for e in bb.read(kind) if e.id not in replaced]

Now staleness is structural rather than a thing you hope agents remember to check. A draft that a reviser replaced simply stops appearing in the live view, so no downstream agent reasons over it, and yet it is still on the board when you need to answer "what did the writer produce before the critic's second pass?" That question is unanswerable in the accidental version, and it is the exact question you will be asking at 2am when an output went wrong. Pairing this with real tracing, along the lines of OpenTelemetry spans for agent runs, turns the board's provenance into something you can actually query in production.

Where it fits and where it does not

The blackboard earns its complexity when multiple specialists contribute to one evolving artifact and the running order depends on the state. Research assistants, document pipelines with revision loops, planning systems where new information reopens earlier decisions. Anywhere the honest answer to "which agent runs next?" is "it depends on what we know so far."

It is the wrong tool for a short fixed pipeline. If agent A always feeds B feeds C and that will never change, a direct chain is clearer and you should not add a control loop to schedule a sequence you already know. It also does not save you from designing your entry kinds carefully; an untyped blackboard is just the accidental mess with extra ceremony. And it can absolutely balloon token cost if you let agents read the whole board, so scoping reads is not optional, it is the thing that makes the pattern affordable.

The mistake is not choosing a blackboard or choosing handoffs. The mistake is sliding into a blackboard without the two parts that make it work, then blaming the pattern for the mess that missing control and missing provenance created.

The takeaway

If your multi-agent system has grown a shared state object that everybody reads and writes, you already have a blackboard, you just have the version that will be unmaintainable by the tenth agent. Making it deliberate is cheap: type the entries, record who wrote each one and what it came from, supersede instead of mutate, and put a real control component in charge of who runs next. That is a few dozen lines, and it is the difference between a system you can extend and audit and one you are afraid to touch.

If you are staring at an agent graph that has turned into a mesh and every new capability means rewiring the old ones, that is usually a structural fix rather than a rewrite. Book a consultation call and we can look at whether your system wants a blackboard, and which two parts of it you are currently missing.

Viral Ruparel

Generative AI consultant helping teams ship reliable LLM and agent systems in production.

Contact Viral about your AI project →

Frequently Asked Questions

What is the blackboard pattern in multi-agent AI systems?+

It is an architecture where agents do not message each other directly. Instead they read from and write to a shared store called the blackboard. Each agent watches the board for state it knows how to act on, contributes its result back, and a separate control component decides which agent runs next. It is a classic AI pattern from the 1970s and 80s that multi-agent LLM systems keep rediscovering, because it decouples agents from each other so you can add a new one without rewiring the existing ones.

When should I use a blackboard instead of direct agent handoffs?+

Use direct handoffs when the flow is a short, fixed pipeline where agent A always feeds agent B. Reach for a blackboard when several agents contribute to one evolving problem, when the order they should run in depends on the state rather than a fixed script, or when you expect to keep adding agents. The signal that you need it is usually that your handoff wiring has started to look like a mesh, where every new agent has to know about several others.

What makes an accidental blackboard become a mess?+

Three things, usually. Entries are untyped, so agents guess at the shape of what they read. There is no provenance, so you cannot tell which agent produced a value or what it was derived from when it turns out wrong. And there is no control component, so every agent runs on every tick and reads stale state. A deliberate blackboard fixes all three with typed entries, recorded authorship and derivation, and a scheduler that picks one agent at a time.

How does a blackboard handle stale state?+

By superseding instead of mutating. An agent never edits an existing entry. It writes a new entry that points back at the one it replaces, and readers ask for the live view that filters out superseded parents. The old entries stay on the board as an audit trail so you can reconstruct exactly what the system believed at each step, which is what makes a failure debuggable after the fact.

Does a blackboard architecture increase token cost?+

It can, if every agent reads the entire board into its prompt on every turn. The fix is scoping. Agents subscribe to the entry kinds they actually need rather than the whole board, and the live view drops superseded entries before they reach a context window. Done that way, a blackboard often costs less than point-to-point handoffs that repeatedly re-serialize the full state into each message.

Related Articles

AI Engineering

Your Voice Agent's Dead Air Is an Architecture Problem

A voice agent that goes silent for two seconds after the caller stops talking feels broken, and no faster model fixes it, because the floor is retrieval plus generation plus speech. The fix is structural: run a fast loop that owns the microphone and the caller's attention, and a slow loop that does the real work behind it. This is how to split them, handle barge-in cleanly, and warm the expensive work before the caller has finished the sentence.

AI Engineering

Your Easy Queries Are Paying for Thinking They Never Use

You turned on extended thinking because it lifted your quality numbers, and a quarter later the invoice had doubled. The reason is boring: most of your traffic is easy, and you are buying every one of those easy requests a slow, expensive reasoning path it never needed. Reasoning effort is a per-query decision now, not a global switch, and treating it that way buys back most of the bill without touching quality on the requests that actually matter.

AI Engineering

One Flaky Step Is Sinking Your Agent. Vote It Out.

You measured your agent and found one step that flips between right and wrong on the same input. Fine-tuning is a project, and swapping to the frontier model on every call blows the budget. There's a third option that most teams skip: run the shaky step several times in parallel and take the consensus. The error math is brutal in your favor, but only if you avoid the trap where wrong answers agree just as loudly as right ones.