Skip to Content
DocsAPI classesArchitecturesBlackboardArchitecture

BlackboardArchitecture

from loomflow.architecture import BlackboardArchitecture, Blackboard, BlackboardEntry

Coordinator + 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) AND BlackboardArchitecture (the architecture class). The dataclass Blackboard is a separate type used internally for the shared workspace state. This page documents BlackboardArchitecture; the Blackboard workspace 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

Typedict[str, Agent]
Defaultrequired

Contributing agents keyed by name. Must be non-empty. The coordinator emits one of these names each round to pick the next contributor.

coordinator

TypeAgent | None
DefaultNone

The agent that picks who acts next each round. When None, the architecture falls back to a simple round-robin over agents.keys().

decider

TypeAgent | None
DefaultNone

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

Typeint
Default10

Maximum rounds before terminating. Must be >= 1. Hitting the cap runs the decider on whatever’s on the board so far.

coordinator_instructions

Typestr | None
DefaultNone (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

Typestr | None
DefaultNone (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 workers

Contributing agents plus the coordinator / decider when present. The __ prefix marks them as architectural roles rather than contributors.

run

  1. Initialize a Blackboard() and post the user’s prompt as the first entry (kind "problem").
  2. 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.
  3. Decide. Either the fallback (last answer-kind / most recent) or decider.run(decide_prompt). Write to session.output.

Per-round events: blackboard.coordinator_started, blackboard.coordinator_decided, blackboard.contribution.


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"
FieldTypeDefaultDescription
timestampdatetimerequiredUTC timestamp at posting time.
authorstrrequiredAgent name (or "user" / "coordinator" / "decider" for architectural roles).
contentstrrequiredThe contribution text.
kindstr"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.

Last updated on