Skip to Content
API classesArchitecturesBlackboardArchitecture

BlackboardArchitecture

from loomflow.architecture import BlackboardArchitecture, Blackboard, BlackboardEntry

BlackboardArchitecture has several agents work on a shared board. Each round, a coordinator picks which agent adds to the board next; when the work is done, a decider writes the final answer from everything on it. Use it for investigations where partial findings from different agents build on each other, and neither a fixed hierarchy (Supervisor) nor peer handoffs (Swarm) fits. The idea comes from classical AI (Erman et al. 1980, Hearsay-II) and was applied to LLM agents by Han & Zhang 2025; Salemi et al. 2026 report 13–57% relative improvement on data-discovery tasks.

For the conceptual page see Blackboard.

Naming note. BlackboardArchitecture is the architecture class. Blackboard is a separate dataclass holding the shared board state. This page documents BlackboardArchitecture; the Blackboard 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, worker_registry: dict[str, Any] | None = None, role_to_worker_id: dict[str, 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, as JSON (see Coordinator decisions). The prompt is filled with str.format(agents=...), so write any literal braces as {{ and }}; the board is appended after it.

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.

worker_registry / role_to_worker_id

Internal wiring set by Team.blackboard when persistent_subagents=True (the default), so each agent keeps one conversation per user across rounds. You normally don’t pass these yourself.


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. Warn as needed. If the budget stops the run, the output is the board’s latest contribution (the decider is not called).
    • Coordinate. Either round-robin (no coordinator) or coordinator.run(coordinate_prompt) returning a decision. Stream the coordinator’s events.
    • If terminate is true, leave the loop. With no next_agent, or an unknown one, skip the round.
    • Contribute. agents[next_agent].run(contribution_prompt) with the blackboard view and the coordinator’s instruction 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.

Events: blackboard.started, then per round blackboard.coordinator_decided, blackboard.invoking and blackboard.contribution (or blackboard.no_contributor / blackboard.unknown_agent), and finally blackboard.completed.

Coordinator decisions

The coordinator answers with a JSON object:

{"terminate": false, "next_agent": "evidence", "instruction": "Find churn data for March."}

The JSON is found even inside prose or a code fence. terminate may be a boolean or a string: "false", "no" and "0" are false. "next_agent": "null" (or "none", or an empty string) means no contributor this round. A reply with no JSON object is treated the same way: the round is skipped, not the run. (Before 0.17, "terminate": "false" ended the run.)


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", private_to: str | None = None, ) -> BlackboardEntry: ... def render_for(self, agent_name: str) -> str: ...

The shared board. post adds an entry to the public list, or to one agent’s private list with private_to=. render_for(name) formats the public entries plus that agent’s private ones for its prompt.

BlackboardEntry

@dataclass class BlackboardEntry: timestamp: datetime author: str content: str kind: str = "contribution"
FieldTypeDefaultDescription
timestampdatetimerequiredUTC timestamp at posting time.
authorstrrequiredAgent name, "user" for the problem statement, or "system" for an error note (such as an unknown agent picked).
contentstrrequiredThe contribution text.
kindstr"contribution"A tag such as "problem", "contribution", "answer" or "error". Without a decider, the last "answer" entry wins over generic contributions.

When Blackboard pays off

  • Data discovery and investigation: agents contribute partial findings that build on each other.
  • Hypothesis generation and verification: propose, search and 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 JSON; see Coordinator decisions for the exact shape.

Last updated on