Blackboard Architecture for AI Agents
Blackboard architecture is one of the oldest multi-agent coordination patterns — and one of the most underused in LLM systems. When agents need to collaborate without tight coupling, a shared workspace outperforms direct message passing.
import json
import time
from anthropic import Anthropic
client = Anthropic()
class Blackboard:
def __init__(self):
self.state = {
"problem": "",
"facts": [],
"hypotheses": [],
"evidence": [],
"conclusion": None,
"confidence": 0.0,
"history": []
}
def read(self) -> dict:
return self.state.copy()
def write(self, updates: dict, agent_name: str):
for key, value in updates.items():
if key in ("facts", "hypotheses", "evidence"):
# Append to lists rather than overwrite
self.state[key].extend(value if isinstance(value, list) else [value])
else:
self.state[key] = value
self.state["history"].append({
"agent": agent_name,
"timestamp": time.time(),
"updates": list(updates.keys())
})
def summary(self) -> str:
"""Compact representation for agent context windows."""
return json.dumps({
"problem": self.state["problem"],
"facts_count": len(self.state["facts"]),
"facts": self.state["facts"][-5:], # Most recent 5
"hypotheses": self.state["hypotheses"],
"conclusion": self.state["conclusion"],
"confidence": self.state["confidence"]
}, indent=2)
def research_agent(blackboard: Blackboard) -> bool:
"""Contributes when facts list is empty or thin."""
state = blackboard.read()
if len(state["facts"]) >= 5:
return False # No contribution needed
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system="You are a research agent. Extract 3-5 specific, verifiable facts relevant to the problem. Return JSON: {\"facts\": [\"fact1\", ...]}",
messages=[{"role": "user", "content": f"Problem: {state['problem']}\n\nCurrent facts: {state['facts']}\n\nAdd new facts not already listed."}]
)
data = json.loads(response.content[0].text)
blackboard.write({"facts": data["facts"]}, "researcher")
return True
def hypothesis_agent(blackboard: Blackboard) -> bool:
"""Contributes when facts exist but hypotheses are sparse."""
state = blackboard.read()
if len(state["facts"]) < 3 or len(state["hypotheses"]) >= 3:
return False
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system="You are a hypothesis agent. Generate 2-3 candidate explanations or solutions based on available facts. Return JSON: {\"hypotheses\": [\"hypothesis1\", ...]}",
messages=[{"role": "user", "content": f"Problem: {state['problem']}\nFacts: {json.dumps(state['facts'])}\n\nGenerate hypotheses not already listed: {state['hypotheses']}"}]
)
data = json.loads(response.content[0].text)
blackboard.write({"hypotheses": data["hypotheses"]}, "hypothesis_generator")
return True
def evidence_agent(blackboard: Blackboard) -> bool:
"""Evaluates hypotheses against facts."""
state = blackboard.read()
if not state["hypotheses"] or len(state["evidence"]) >= len(state["hypotheses"]):
return False
unevaluated = state["hypotheses"][len(state["evidence"]):]
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system="You are an evidence evaluator. Assess how well the facts support each hypothesis. Return JSON: {\"evidence\": [{\"hypothesis\": \"...\", \"support\": \"strong|moderate|weak\", \"reasoning\": \"...\"}]}",
messages=[{"role": "user", "content": f"Facts: {json.dumps(state['facts'])}\nEvaluate these hypotheses: {json.dumps(unevaluated)}"}]
)
data = json.loads(response.content[0].text)
blackboard.write({"evidence": data["evidence"]}, "evidence_evaluator")
return True
def conclusion_agent(blackboard: Blackboard) -> bool:
"""Produces conclusion when evidence is complete."""
state = blackboard.read()
if (len(state["evidence"]) < len(state["hypotheses"]) or
not state["hypotheses"] or
state["conclusion"] is not None):
return False
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system="You are a synthesis agent. Produce a final conclusion and confidence score. Return JSON: {\"conclusion\": \"...\", \"confidence\": 0.0-1.0}",
messages=[{"role": "user", "content": f"Problem: {state['problem']}\nEvidence: {json.dumps(state['evidence'])}"}]
)
data = json.loads(response.content[0].text)
blackboard.write({"conclusion": data["conclusion"], "confidence": data["confidence"]}, "synthesizer")
return True
def run_blackboard_system(problem: str, max_cycles: int = 8) -> dict:
board = Blackboard()
board.write({"problem": problem}, "system")
agents = [research_agent, hypothesis_agent, evidence_agent, conclusion_agent]
for cycle in range(max_cycles):
state = board.read()
if state["conclusion"] is not None and state["confidence"] > 0.7:
break
acted = False
for agent in agents:
if agent(board):
acted = True
break # One agent per cycle
if not acted:
break # No agent had anything to contribute
return board.read()
result = run_blackboard_system("Why might a microservice application have increasing memory usage under constant load?")
print(f"Conclusion: {result['conclusion']}")
print(f"Confidence: {result['confidence']}")
print(f"Agent history: {[h['agent'] for h in result['history']]}")The blackboard multi agent architecture originated in AI research in the 1970s, designed for problems where no single solving strategy was adequate and multiple knowledge sources needed to cooperate without being tightly coupled. The name is literal: agents coordinate by reading and writing to a shared data structure — the blackboard — rather than passing messages directly to each other.
In LLM-based systems, the blackboard pattern solves a specific coordination problem: how do multiple agents contribute to a shared solution when the order of their contributions depends on what's already been discovered? Direct message passing requires knowing in advance which agent should run next. The blackboard pattern lets agents self-select based on what the current workspace state needs.
How Blackboard Architecture Works
The blackboard pattern has three components:
The blackboard: A shared, mutable data structure that holds the current state of the problem — partial solutions, intermediate findings, hypotheses, and any supporting evidence agents have contributed. All agents read from and write to the same blackboard.
Knowledge sources (agents): Specialized agents that watch the blackboard for conditions they can act on. When an agent detects that the current blackboard state matches its capability — it has something to contribute — it applies its knowledge and updates the blackboard.
The control component (scheduler): Determines which knowledge source should act next, based on what the blackboard currently needs. The scheduler can be a separate agent, a rule-based system, or a simple priority queue.
The key structural difference from sequential pipelines: in a sequential pipeline, the orchestrator decides upfront what happens next. In blackboard architecture, agents respond to the current state of the shared workspace.
⚡ Pro tip: Blackboard architecture excels when the problem-solving path isn't predictable upfront. If you can enumerate the exact sequence of operations that solves your task, a sequential pipeline is simpler. Use blackboard when the path depends on intermediate discoveries — when what you find in step 2 determines whether step 3 is needed and which step 4 makes sense.
Implementing a Blackboard System in Python
[object Object], json
,[object Object], time
,[object Object], anthropic ,[object Object], Anthropic
client = Anthropic()
,[object Object], ,[object Object],:
,[object Object], ,[object Object],(,[object Object],):
,[object Object],.state = {
,[object Object],: ,[object Object],,
,[object Object],: [],
,[object Object],: [],
,[object Object],: [],
,[object Object],: ,[object Object],,
,[object Object],: ,[object Object],,
,[object Object],: []
}
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
,[object Object], ,[object Object],.state.copy()
,[object Object], ,[object Object],(,[object Object],):
,[object Object], key, value ,[object Object], updates.items():
,[object Object], key ,[object Object], (,[object Object],, ,[object Object],, ,[object Object],):
,[object Object],
,[object Object],.state[key].extend(value ,[object Object], ,[object Object],(value, ,[object Object],) ,[object Object], [value])
,[object Object],:
,[object Object],.state[key] = value
,[object Object],.state[,[object Object],].append({
,[object Object],: agent_name,
,[object Object],: time.time(),
,[object Object],: ,[object Object],(updates.keys())
})
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
,[object Object],
,[object Object], json.dumps({
,[object Object],: ,[object Object],.state[,[object Object],],
,[object Object],: ,[object Object],(,[object Object],.state[,[object Object],]),
,[object Object],: ,[object Object],.state[,[object Object],][-,[object Object],:], ,[object Object],
,[object Object],: ,[object Object],.state[,[object Object],],
,[object Object],: ,[object Object],.state[,[object Object],],
,[object Object],: ,[object Object],.state[,[object Object],]
}, indent=,[object Object],)
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
,[object Object],
state = blackboard.read()
,[object Object], ,[object Object],(state[,[object Object],]) >= ,[object Object],:
,[object Object], ,[object Object], ,[object Object],
response = client.messages.create(
model=,[object Object],,
max_tokens=,[object Object],,
system=,[object Object],,
messages=[{,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}]
)
data = json.loads(response.content[,[object Object],].text)
blackboard.write({,[object Object],: data[,[object Object],]}, ,[object Object],)
,[object Object], ,[object Object],
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
,[object Object],
state = blackboard.read()
,[object Object], ,[object Object],(state[,[object Object],]) < ,[object Object], ,[object Object], ,[object Object],(state[,[object Object],]) >= ,[object Object],:
,[object Object], ,[object Object],
response = client.messages.create(
model=,[object Object],,
max_tokens=,[object Object],,
system=,[object Object],,
messages=[{,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}]
)
data = json.loads(response.content[,[object Object],].text)
blackboard.write({,[object Object],: data[,[object Object],]}, ,[object Object],)
,[object Object], ,[object Object],
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
,[object Object],
state = blackboard.read()
,[object Object], ,[object Object], state[,[object Object],] ,[object Object], ,[object Object],(state[,[object Object],]) >= ,[object Object],(state[,[object Object],]):
,[object Object], ,[object Object],
unevaluated = state[,[object Object],][,[object Object],(state[,[object Object],]):]
response = client.messages.create(
model=,[object Object],,
max_tokens=,[object Object],,
system=,[object Object],,
messages=[{,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}]
)
data = json.loads(response.content[,[object Object],].text)
blackboard.write({,[object Object],: data[,[object Object],]}, ,[object Object],)
,[object Object], ,[object Object],
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
,[object Object],
state = blackboard.read()
,[object Object], (,[object Object],(state[,[object Object],]) < ,[object Object],(state[,[object Object],]) ,[object Object],
,[object Object], state[,[object Object],] ,[object Object],
state[,[object Object],] ,[object Object], ,[object Object], ,[object Object],):
,[object Object], ,[object Object],
response = client.messages.create(
model=,[object Object],,
max_tokens=,[object Object],,
system=,[object Object],,
messages=[{,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}]
)
data = json.loads(response.content[,[object Object],].text)
blackboard.write({,[object Object],: data[,[object Object],], ,[object Object],: data[,[object Object],]}, ,[object Object],)
,[object Object], ,[object Object],
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
board = Blackboard()
board.write({,[object Object],: problem}, ,[object Object],)
agents = [research_agent, hypothesis_agent, evidence_agent, conclusion_agent]
,[object Object], cycle ,[object Object], ,[object Object],(max_cycles):
state = board.read()
,[object Object], state[,[object Object],] ,[object Object], ,[object Object], ,[object Object], ,[object Object], state[,[object Object],] > ,[object Object],:
,[object Object],
acted = ,[object Object],
,[object Object], agent ,[object Object], agents:
,[object Object], agent(board):
acted = ,[object Object],
,[object Object], ,[object Object],
,[object Object], ,[object Object], acted:
,[object Object], ,[object Object],
,[object Object], board.read()
result = run_blackboard_system(,[object Object],)
,[object Object],(,[object Object],)
,[object Object],(,[object Object],)
,[object Object],(,[object Object],)What this does: Each agent checks the blackboard state and decides whether it has something to contribute. The research agent fills facts. The hypothesis agent generates explanations once enough facts exist. The evidence agent evaluates hypotheses once they're generated. The conclusion agent synthesizes once evidence is complete. The scheduler (the run_blackboard_system loop) cycles through agents and stops when a conclusion with sufficient confidence is reached, or when no agent has anything to add.
⚡ Pro tip: The blackboard summary method is critical for production systems. Full blackboard state passed to every agent grows exponentially with problem complexity. Build compact summary representations that give agents the context they need without flooding their context windows with data they won't use.
When Blackboard Multi Agent Beats Direct Messaging
The blackboard multi agent pattern outperforms direct message passing in three scenarios:
Open-ended investigation tasks. When you don't know upfront how many agents need to contribute or what order they should run in, the blackboard pattern lets agents self-select based on the current state of knowledge. A research pipeline where early findings might trigger additional research, new hypothesis generation, or immediate conclusions benefits from this flexibility.
Collaborative refinement. When multiple agents can improve a shared artifact iteratively — a draft document that a researcher improves, a critic annotates, and a reviser refines across multiple passes — the blackboard provides a natural shared workspace. Each agent contributes when the current state of the artifact matches its capability.
Asynchronous agent execution. In systems where agents run at different speeds or have different availability, the blackboard decouples producers from consumers. A fast agent doesn't wait for a slow one; it writes what it knows and checks back when the slow agent has contributed.
Where Blackboard Architecture Adds Complexity Without Value
For sequential pipelines with a fixed, known task order, blackboard architecture adds unnecessary complexity. If your pipeline is always: classify → research → analyze → report, a simple sequential orchestrator is easier to reason about and debug. The blackboard earns its complexity when the path through the problem is discovered, not predefined.
⚠️ Common mistake: Implementing a blackboard system without a confidence threshold or termination condition. Without a stopping criterion, the system will cycle until it hits max_cycles even after reaching a correct answer, wasting API calls and adding latency. Always define what "done" means before the system starts.
Connecting Blackboard to Modern Agent Systems
The blackboard pattern maps directly to how several modern multi-agent frameworks handle shared state. LangGraph's state object is a structured blackboard. CrewAI's shared context between tasks is a simplified blackboard. The underlying concept predates these frameworks by decades because it solves a real coordination problem.
Implementing a blackboard system from scratch is not mandatory — using LangGraph's state for a blackboard-style workflow is a legitimate approach. The value of understanding the raw blackboard pattern is conceptual: it explains why LangGraph's state-centric design works, why CrewAI's task context passing feels natural, and what both are trying to achieve. When a framework's built-in state management isn't flexible enough for your coordination problem, building a custom blackboard gives you the full pattern without the framework's constraints.
In LLM applications, the most practical implementation keeps the blackboard as a typed Python dictionary with append-only list fields and scalar overwrite fields. Full event sourcing (storing every write as an event) is useful for debugging complex systems but unnecessary for most applications.
Blackboard Pattern for Knowledge Discovery Tasks
The blackboard architecture excels at open-ended knowledge discovery where the path to the answer isn't known in advance. Consider a root cause analysis system for software incidents: the problem is known (the system is slow), but whether the cause is a database issue, a memory leak, a network bottleneck, or an external service dependency isn't known until specialist agents investigate.
[object Object], json
,[object Object], anthropic ,[object Object], Anthropic
client = Anthropic()
,[object Object], ,[object Object],:
,[object Object], ,[object Object],(,[object Object],):
,[object Object],.state = {
,[object Object],: incident,
,[object Object],: [],
,[object Object],: [],
,[object Object],: {},
,[object Object],: [],
,[object Object],: ,[object Object],
}
,[object Object], ,[object Object],(,[object Object],):
,[object Object],.state[,[object Object],].extend(symptoms)
,[object Object],(,[object Object],)
,[object Object], ,[object Object],(,[object Object],):
,[object Object],.state[,[object Object],].append({
,[object Object],: hypothesis, ,[object Object],: confidence, ,[object Object],: agent
})
,[object Object], ,[object Object],(,[object Object],):
key = hypothesis[:,[object Object],]
,[object Object], key ,[object Object], ,[object Object], ,[object Object],.state[,[object Object],]:
,[object Object],.state[,[object Object],][key] = []
,[object Object],.state[,[object Object],][key].append({
,[object Object],: evidence, ,[object Object],: supports, ,[object Object],: agent
})
,[object Object], ,[object Object],(,[object Object],):
,[object Object],.state[,[object Object],].append({,[object Object],: hypothesis, ,[object Object],: reason})
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
active_hypotheses = [
h ,[object Object], h ,[object Object], ,[object Object],.state[,[object Object],]
,[object Object], h[,[object Object],][:,[object Object],] ,[object Object], ,[object Object], [r[,[object Object],][:,[object Object],] ,[object Object], r ,[object Object], ,[object Object],.state[,[object Object],]]
]
,[object Object], json.dumps({
,[object Object],: ,[object Object],.state[,[object Object],],
,[object Object],: ,[object Object],.state[,[object Object],][-,[object Object],:],
,[object Object],: active_hypotheses,
,[object Object],: ,[object Object],(,[object Object],(v) ,[object Object], v ,[object Object], ,[object Object],.state[,[object Object],].values()),
,[object Object],: ,[object Object],(,[object Object],.state[,[object Object],])
}, indent=,[object Object],)
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
,[object Object], ,[object Object],(board.state[,[object Object],]) >= ,[object Object],:
,[object Object], ,[object Object],
response = client.messages.create(
model=,[object Object],, max_tokens=,[object Object],,
system=,[object Object],,
messages=[{,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}]
)
data = json.loads(response.content[,[object Object],].text)
board.add_symptoms(data[,[object Object],], ,[object Object],)
,[object Object], ,[object Object],
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
,[object Object], ,[object Object],(board.state[,[object Object],]) < ,[object Object], ,[object Object], ,[object Object],(board.state[,[object Object],]) >= ,[object Object],:
,[object Object], ,[object Object],
symptom_text = ,[object Object],.join(board.state[,[object Object],])
response = client.messages.create(
model=,[object Object],, max_tokens=,[object Object],,
system=,[object Object],,
messages=[{,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}]
)
data = json.loads(response.content[,[object Object],].text)
,[object Object], h ,[object Object], data[,[object Object],]:
board.add_hypothesis(h[,[object Object],], h[,[object Object],], ,[object Object],)
,[object Object], ,[object Object],
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
unevaluated = [
h ,[object Object], h ,[object Object], board.state[,[object Object],]
,[object Object], h[,[object Object],][:,[object Object],] ,[object Object], ,[object Object], board.state[,[object Object],]
]
,[object Object], ,[object Object], unevaluated:
,[object Object], ,[object Object],
h = unevaluated[,[object Object],]
response = client.messages.create(
model=,[object Object],, max_tokens=,[object Object],,
system=,[object Object],,
messages=[{,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}]
)
data = json.loads(response.content[,[object Object],].text)
board.add_evidence(h[,[object Object],], data[,[object Object],], data[,[object Object],], ,[object Object],)
,[object Object], ,[object Object], data[,[object Object],] ,[object Object], data[,[object Object],] > ,[object Object],:
board.rule_out(h[,[object Object],], data[,[object Object],])
,[object Object], ,[object Object],
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
board = IncidentBlackboard(incident)
agents = [symptom_collector, hypothesis_generator, evidence_evaluator]
,[object Object], cycle ,[object Object], ,[object Object],(max_cycles):
acted = ,[object Object],
,[object Object], agent ,[object Object], agents:
,[object Object], agent(board):
acted = ,[object Object],
,[object Object],
,[object Object], ,[object Object], acted:
,[object Object],
,[object Object], board.stateWhat this does: The incident blackboard accumulates symptoms, generates hypotheses based on what's been observed, and evaluates evidence for each hypothesis — ruling out hypotheses where evidence clearly contradicts them. Each agent contributes when it has something to add; the cycle continues until no agent has a contribution. The blackboard's state at termination is the investigation record: what was observed, what was hypothesized, what was ruled out, and what remains active.
⚡ Pro tip: Initialize the blackboard with any structured data you already have before starting the agent cycle. System metrics, log samples, alert history — seed the blackboard with this data so the symptom collector starts with a richer information set than it would have from the incident description alone. Richer initial state produces better hypotheses in the first few cycles.
Blackboard vs Event-Driven Architecture
Both blackboard and event-driven architectures allow agents to coordinate without tight coupling. The key difference: in event-driven systems, each event triggers specific handler agents. In blackboard systems, agents self-select based on the current state of a shared workspace.
Event-driven architecture is better for: well-defined trigger conditions where the relationship between an event and its handler is fixed; high-throughput systems where many events need to be processed efficiently; and pipelines where the event sequence is predictable.
Blackboard architecture is better for: open-ended problems where agent contribution order isn't predictable; tasks where agents need to read the full investigation state (not just the triggering event); and collaborative refinement where multiple agents improve the same shared artifact.
Scaling Blackboard Coordination
As the number of knowledge sources (agents) grows beyond five or six, the per-cycle agent polling becomes a bottleneck. Each cycle checks every agent to find who has something to contribute. At ten agents, this is ten LLM calls per cycle to identify one contributing agent — nine of which return "no contribution needed."
Two scaling strategies address this:
Condition-based subscription. Rather than polling all agents each cycle, agents register the blackboard conditions that trigger their contribution. When the blackboard state changes, only agents subscribed to the changed condition are polled. This reduces polling overhead from N agents per cycle to only the agents whose contribution conditions might now be met.
Prioritized agent ordering. Within each cycle, agents that have contributed recently or that have high-priority contributions are polled first. If a high-priority agent has something to contribute, lower-priority agents aren't polled in that cycle. This is a heuristic optimization that works well when there's a natural priority order among knowledge sources.
[object Object], ,[object Object],:
,[object Object], ,[object Object],(,[object Object],):
,[object Object],.state = {}
,[object Object],.agent_priorities = {}
,[object Object],.contribution_counts = {}
,[object Object], ,[object Object],(,[object Object],):
,[object Object],.agent_priorities[agent_name] = priority
,[object Object],
,[object Object], ,[object Object],(,[object Object],) -> ,[object Object],:
ready = [
name ,[object Object], name, _ ,[object Object], ,[object Object],(
,[object Object],.agent_priorities.items(), key=,[object Object], x: x[,[object Object],], reverse=,[object Object],
)
]
,[object Object], readyWhat this does: The PrioritizedBlackboard tracks agent priorities and sorts polling order accordingly. High-priority agents (typically those with narrower, more certain contribution conditions) are polled first. Once any high-priority agent contributes, the cycle ends — lower-priority agents wait for the next cycle. This preserves the blackboard's self-organizing property while reducing the per-cycle polling cost. For systems with ten or more knowledge sources, combining priority ordering with condition-based filtering typically reduces polling overhead by sixty to eighty percent while maintaining the same contribution quality. Start with simple round-robin polling during development — add prioritization only when polling overhead becomes a measurable latency factor in production.
The agent definitions for a well-functioning blackboard system — their contribution conditions, their output schemas, their context summaries — are worth saving across projects. The blackboard pattern appears in research, diagnosis, collaborative writing, and investigation tasks. A well-tuned set of blackboard agents transfers with minimal modification. PromptABCD is the natural place to keep them.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
