BlackboardArchitecture
from loomflow.architecture import BlackboardArchitecture, Blackboard, BlackboardEntryCoordinator + agents + decider, mediated by a shared blackboard. Classical AI: Erman et al. 1980 (Hearsay-II). Han & Zhang 2025 revived for LLM agents. Salemi et al. 2026 reports +13–57% relative improvement on data-discovery tasks.
For the conceptual page see Blackboard.
Naming note. The public re-export is
Blackboard(alias) ANDBlackboardArchitecture(the architecture class). The dataclassBlackboardis a separate type used internally for the shared workspace state. This page documentsBlackboardArchitecture; theBlackboardworkspace type is documented at the bottom.
Class signature
class BlackboardArchitecture:
name: str = "blackboard"
def __init__(
self,
*,
agents: dict[str, Agent],
coordinator: Agent | None = None,
decider: Agent | None = None,
max_rounds: int = 10,
coordinator_instructions: str | None = None,
decider_instructions: str | None = None,
) -> None: ...Constructor parameters
agents
| Type | dict[str, Agent] |
| Default | required |
Contributing agents keyed by name. Must be non-empty. The coordinator emits one of these names each round to pick the next contributor.
coordinator
| Type | Agent | None |
| Default | None |
The agent that picks who acts next each round. When None, the
architecture falls back to a simple round-robin over agents.keys().
decider
| Type | Agent | None |
| Default | None |
The agent that synthesizes the final answer from the full blackboard.
When None, the architecture falls back to: the last
answer-kind contribution, or the most recent contribution if no
answer-kind exists.
max_rounds
| Type | int |
| Default | 10 |
Maximum rounds before terminating. Must be >= 1. Hitting the cap
runs the decider on whatever’s on the board so far.
coordinator_instructions
| Type | str | None |
| Default | None (uses built-in default) |
Override the coordinator’s prompt. The default reads the blackboard and asks the model to either pick the next agent (by name) or terminate.
decider_instructions
| Type | str | None |
| Default | None (uses built-in default) |
Override the decider’s prompt. The default reads the full blackboard and produces the final answer.
Methods
declared_workers
def declared_workers(self) -> dict[str, Agent]:
workers = dict(self._agents)
if self._coordinator is not None:
workers["__coordinator"] = self._coordinator
if self._decider is not None:
workers["__decider"] = self._decider
return workersContributing agents plus the coordinator / decider when present.
The __ prefix marks them as architectural roles rather than
contributors.
run
- Initialize a
Blackboard()and post the user’s prompt as the first entry (kind"problem"). - For each round (1..max_rounds):
- Budget check. Block / warn as needed.
- Coordinate. Either round-robin (no coordinator) or
coordinator.run(coordinate_prompt)returning a structured decision:{terminate: bool, next_agent: str}. Stream the coordinator’s events. - If
terminate=True→ break the loop. - Contribute.
agents[next_agent].run(contribution_prompt)with the blackboard view in its prompt. Append the output to the blackboard.
- Decide. Either the fallback (last answer-kind / most recent)
or
decider.run(decide_prompt). Write tosession.output.
Per-round events: blackboard.coordinator_started,
blackboard.coordinator_decided, blackboard.contribution.
Related types
Blackboard
@dataclass
class Blackboard:
public: list[BlackboardEntry] = field(default_factory=list)
private: dict[str, list[BlackboardEntry]] = field(default_factory=dict)
def post(
self, author: str, content: str, *, kind: str = "contribution",
) -> BlackboardEntry: ...
def view(self) -> str: ...Internal mutable state. Agents can read the public list and their own private partition; the architecture writes to public.
BlackboardEntry
@dataclass
class BlackboardEntry:
timestamp: datetime
author: str
content: str
kind: str = "contribution"| Field | Type | Default | Description |
|---|---|---|---|
timestamp | datetime | required | UTC timestamp at posting time. |
author | str | required | Agent name (or "user" / "coordinator" / "decider" for architectural roles). |
content | str | required | The contribution text. |
kind | str | "contribution" | Tag, "problem", "contribution", "answer", etc. The default decider fallback prefers "answer" over generic contributions. |
When Blackboard pays off
- Data-discovery / investigation. Agents contribute partial findings that compound on the board.
- Hypothesis generation + verification. Propose / search / critique cycles.
- Loose coordination. When neither hierarchical (Supervisor) nor peer-handoff (Swarm) fits.
Cost: 3–5× single-agent. Reserve for tasks where the shared workspace genuinely accelerates over hierarchical delegation.
Example
from loomflow import Agent
from loomflow.team import Team
hypothesis = Agent("Propose hypotheses about the data.", model="claude-opus-4-7")
evidence = Agent("Search for evidence and add it to the board.",
model="claude-opus-4-7", tools=[...])
critic = Agent("Critique the current top hypothesis.", model="gpt-4o")
coordinator = Agent(
"Read the blackboard. Decide which agent contributes next, or "
"terminate when the problem is solved.",
model="claude-opus-4-7",
)
decider = Agent(
"Read the full blackboard and produce the final answer.",
model="claude-opus-4-7",
)
team = Team.blackboard(
agents={"hypothesis": hypothesis, "evidence": evidence, "critic": critic},
coordinator=coordinator,
decider=decider,
model="claude-opus-4-7",
)
result = await team.run("Investigate why retention dropped in March.")Source
loomflow/architecture/blackboard.py
Coordinator API. The coordinator is itself an Agent that
returns one of: a worker name to call next, or a terminate signal.
The framework parses the coordinator’s structured output. See the
docstring on BlackboardArchitecture for the exact protocol.