Tutorial 10: Blackboard — Shared Markdown Pipeline¶
The blackboard is a persistent markdown buffer that nodes can read from
and write to during a single compile() run. It implements the classic
Blackboard architectural pattern:
a shared workspace where multiple agents contribute and consume content.
KeGAL supports multiple named boards in the same graph, each with its own file, cleanup behaviour, and optional import chain.
1. Basic: single board, three-node pipeline¶
Every board interaction is declared with three fields on the node:
- id — which board to access (must match a BlackboardEntry.id)
- read — whether to inject the board's current content as {blackboard}
- write — whether to append the node's response to the board after execution
This produces three natural node categories:
| Category | read |
write |
Role |
|---|---|---|---|
| Cat-1 | false |
true |
Writer — seeds the board |
| Cat-2 | true |
true |
Enricher — reads then extends |
| Cat-3 | true |
false |
Reader — consumes the final board |
The compiler infers the execution order automatically: Cat-1 → Cat-2 (in parallel) → Cat-3.
models:
- llm: "ollama"
model: "qwen2.5:7b"
host: "http://localhost:11434"
blackboard:
path: ./ # directory for board files (relative to YAML)
boards:
- id: main
file: BLACKBOARD.md
cleanup: true # truncate at Compiler init (default)
prompts:
- template: # 0 — writer: seeds the board
system_template:
role: |
You are a research assistant.
Write a concise overview of the topic, max 200 words.
prompt_template:
topic: "{user_message}"
- template: # 1 — enricher_a: economic analysis
system_template:
role: |
You are an economic analyst.
Read the overview below and add 3 bullet points about economic implications.
prompt_template:
overview: |
{blackboard}
- template: # 2 — enricher_b: risk analysis
system_template:
role: |
You are a risk analyst.
Read the overview below and add 3 bullet points about key risks.
prompt_template:
overview: |
{blackboard}
- template: # 3 — reader: synthesises the full board
system_template:
role: |
You are a report writer.
Synthesise everything below into an executive summary of max 150 words.
prompt_template:
board: |
{blackboard}
nodes:
- id: "writer" # Cat-1
model: 0
temperature: 0.3
max_tokens: 300
show: false
blackboard:
id: main
read: false
write: true
prompt: { template: 0, user_message: true }
- id: "enricher_a" # Cat-2
model: 0
temperature: 0.5
max_tokens: 256
show: false
blackboard:
id: main
read: true
write: true
prompt: { template: 1 }
- id: "enricher_b" # Cat-2 — runs in parallel with enricher_a
model: 0
temperature: 0.5
max_tokens: 256
show: false
blackboard:
id: main
read: true
write: true
prompt: { template: 2 }
- id: "summarizer" # Cat-3
model: 0
temperature: 0.4
max_tokens: 300
show: true
blackboard:
id: main
read: true
write: false
prompt: { template: 3 }
edges:
- node: "writer"
- node: "enricher_a"
- node: "enricher_b"
- node: "summarizer"
from kegal import Compiler
with Compiler(uri="blackboard_graph.yml") as compiler:
compiler.user_message = "Impact of autonomous vehicles on urban planning."
compiler.compile()
for node in compiler.get_outputs().nodes:
if node.show:
for msg in node.response.messages or []:
print(msg)
After compile() the file BLACKBOARD.md on disk contains the full
accumulated thread: the writer's overview plus both enrichers' additions.
2. Intermediate: cleanup: false — accumulate across runs¶
By default cleanup: true truncates the board file at Compiler
construction. Set cleanup: false to keep existing content and append new
writes on top of it. This is useful for logs, journals, or multi-session
accumulation.
blackboard:
path: ./logs/
boards:
- id: journal
file: journal.md
cleanup: false # keep all previous content
Each compile() call — across separate Compiler instances — appends to
the same file. No content is ever lost between runs.
3. Intermediate: multiple boards¶
Declare multiple boards in the same graph. Nodes reference them by ID. Each board has its own file; different nodes can write to different boards.
blackboard:
path: ./
boards:
- id: facts
file: facts.md
cleanup: true
- id: report
file: report.md
cleanup: true
nodes:
- id: "researcher"
blackboard:
id: facts # writes to facts.md
read: false
write: true
...
- id: "writer"
blackboard:
id: report # writes to report.md
read: true # reads report.md (+ any imports)
write: true
...
- id: "editor"
blackboard:
id: report
read: true
write: false
...
4. Advanced: board import chains¶
The import key on a BlackboardEntry prepends other boards' content at
read time. Files on disk stay separate; only the {blackboard}
placeholder delivered to the node includes the imported content.
blackboard:
path: ./
boards:
- id: facts
file: facts.md
cleanup: true
- id: report
file: report.md
cleanup: true
import: [facts] # when reading 'report', prepend 'facts' first
Assembly order when node reads report:
nodes:
- id: "researcher"
blackboard: { id: facts, read: false, write: true }
...
- id: "writer"
blackboard:
id: report
read: true # {blackboard} = facts content + report content
write: true
prompt:
template: 1 # template uses {blackboard}
5. Advanced: full two-board pipeline¶
A researcher accumulates raw notes into facts. A writer reads both facts
and the current report draft (via import), then extends the draft. An
editor reads the final draft for review.
models:
- llm: "ollama"
model: "qwen2.5:7b"
host: "http://localhost:11434"
blackboard:
path: ./
boards:
- id: facts
file: facts.md
cleanup: true
- id: report
file: report.md
cleanup: true
import: [facts]
prompts:
- template: # 0 — researcher
system_template:
role: Write concise factual notes about the topic. Max 5 bullet points.
prompt_template:
topic: "{user_message}"
- template: # 1 — writer
system_template:
role: |
You are a science writer. The research notes and existing report draft
are below. Add one new paragraph to the article.
prompt_template:
material: |
{blackboard}
- template: # 2 — editor
system_template:
role: |
You are an editor. Review the article below and summarise any
improvements needed in one sentence.
prompt_template:
article: |
{blackboard}
nodes:
- id: "researcher" # Cat-1 on 'facts'
model: 0
temperature: 0.3
max_tokens: 300
show: false
blackboard: { id: facts, read: false, write: true }
prompt: { template: 0, user_message: true }
- id: "writer" # Cat-2 on 'report' (reads facts via import)
model: 0
temperature: 0.5
max_tokens: 500
show: false
blackboard: { id: report, read: true, write: true }
prompt: { template: 1 }
- id: "editor" # Cat-3 on 'report'
model: 0
temperature: 0.3
max_tokens: 256
show: true
blackboard: { id: report, read: true, write: false }
prompt: { template: 2 }
edges:
- node: "researcher"
- node: "writer"
- node: "editor"
6. Reading board content in Python¶
After compile(), board files on disk hold the accumulated text. Read them
directly with standard file I/O:
from pathlib import Path
from kegal import Compiler
with Compiler(uri="blackboard_graph.yml") as compiler:
compiler.user_message = "Autonomous vehicles."
compiler.compile()
# Board files are available after compile() even while inside the `with` block
board_content = Path("BLACKBOARD.md").read_text(encoding="utf-8")
print(board_content)
Key points¶
- Board IDs must be unique within the graph; duplicate IDs raise
ValueErrorat parse time. - Node
blackboard.idmust reference a declared board ID; unknown IDs raiseValueErroratCompilerconstruction. - Cat-1 → Cat-2 (parallel) → Cat-3 ordering is inferred automatically — no
children/fan_indeclarations are needed for the basic pattern. cleanup: true(default) truncates the board at init;cleanup: falsepreserves existing content.importis evaluated at read time — imported boards are prepended to the reading node's{blackboard}but files on disk are never merged.- Empty segments (missing or empty files) are skipped during import assembly.
Related tutorials: 04 Fan-out and fan-in — explicit edge-based parallelism
01 Message passing — alternative data-flow mechanism