BlackboardArchitecture
from loomflow.architecture import BlackboardArchitecture, Blackboard, BlackboardEntryBlackboardArchitecture 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.
BlackboardArchitectureis the architecture class.Blackboardis a separate dataclass holding the shared board state. This page documentsBlackboardArchitecture; theBlackboardtype 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
| 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, 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
| 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.
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 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. 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
terminateis true, leave the loop. With nonext_agent, or an unknown one, skip the round. - Contribute.
agents[next_agent].run(contribution_prompt)with the blackboard view and the coordinator’sinstructionin 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.
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.)
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",
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"| Field | Type | Default | Description |
|---|---|---|---|
timestamp | datetime | required | UTC timestamp at posting time. |
author | str | required | Agent name, "user" for the problem statement, or "system" for an error note (such as an unknown agent picked). |
content | str | required | The contribution text. |
kind | str | "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.