pattern-graphnode-anatomy
Pattern: Graphnode Anatomy
Category: Core Architecture
Status: Active
First Implementation: 2025-11-05
Overview
Graphnodes are the composable function units of the Oculus graph. They enable function-calling semantics within markdown documents, allowing one node to invoke another node's executable fence with parameters, transforming the graph into a programmable knowledge network.
Important: Graphnodes are designed to be called remotely, not viewed directly. Think of them like CPU registers and opcodes:
- Load the registers - Poke parameters into the graphnode's config section
- Call the opcode - Invoke via
graphnode:slug:fetchfrom another node - Read the result - The executed fence returns structured data
Viewing a graphnode directly may show errors like config is not defined because the config injection only happens during remote invocation. This is by design - graphnodes are transform nodes in an instruction set architecture (ISA), not standalone documents.
# The ISA Mental Model
poke('target:config.param1', value1) # Load register 1
poke('target:config.param2', value2) # Load register 2
result = peek('target:fetch') # Execute opcode, get resultThe Mental Model
Think of a graphnode as a function definition embedded in markdown:
┌─────────────────────────────────────────────────┐
│ graphnode-slug (like a function name) │
├─────────────────────────────────────────────────┤
│ ## config │
│ Parameters with default values (like kwargs) │
├─────────────────────────────────────────────────┤
│ ## fetch │
│ The executable code (function body) │
│ Has access to `config` dict │
├─────────────────────────────────────────────────┤
│ ## template │
│ Optional Jinja2 template for default render │
└─────────────────────────────────────────────────┘Graphnode Structure
Required Sections
## config - YAML fence with default parameters:
base_url: http://localhost:7778
timeout: 5
feature_flag: true## fetch - Python fence with [execute=true]:
{
"data": "processed"
}Optional Sections
## template - Jinja2 template for rendering results:
# Results
{% for item in data %}
- {{ item.name }}: {{ item.value }}
{% endfor %}Calling a Graphnode
Basic Syntax
```graphnode:target-slug:section-path
param1: value1
param2: value2
MiddlewareConfig:
option: value
```Anatomy of the Call
graphnode:fortune:fetch
│ │
│ └── Section path (defaults to "fetch")
│
└── Target node slugParameter Passing (Config Injection)
Parameters in the fence content become the config dict in the target's Python fence:
Caller:
```graphnode:poke-test-data
base_url: http://localhost:8080
timeout: 10
```Target's fetch fence sees:
config = {
"base_url": "http://localhost:8080", # Overridden
"timeout": 10, # Overridden
"test_node": "poke-test-fixture" # From default config
}Execution Flow
The Poke-Peek Pattern
Graphnode execution follows the poke-peek pattern - parameters are actually written to the target node's config section before execution:
┌──────────────────────────────────────────────────────────────┐
│ 1. PARSE │
│ Parse fence: graphnode:fortune:fetch │
│ Extract: slug="fortune", path="fetch" │
│ Parse YAML content as params: {source: "stuffy", count: 1}│
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 2. POKE (Parameter Injection) │
│ For each param, actually poke to target node: │
│ poke("fortune:config.source", "stuffy") │
│ poke("fortune:config.count", 1) │
│ This makes params inspectable via peek! │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 3. LOAD TARGET │
│ Load target node's markdown │
│ Find section matching path ("fetch") │
│ Extract fence within that section │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 4. CREATE EXECUTION CONTEXT │
│ FenceExecutionContext( │
│ slug="fortune", │
│ mode="rendered", │
│ graph={'config': merged_params} │
│ ) │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 5. EXECUTE TARGET FENCE │
│ Execute the Python fence with injected config │
│ Fence code accesses config via `config.get('key')` │
│ Result stored in `result` variable │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 6. MIDDLEWARE PIPELINE │
│ Pass result through middleware transformations: │
│ - TableExtractor (if TableConfig present) │
│ - DatetimeNormalizer │
│ - AWSResponseCleaner (for aws:* fences) │
│ - FortuneFormatter (for graphnode:fortune:*) │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 7. RETURN │
│ Return { │
│ "data": result_data, │
│ "metadata": { │
│ "type": "graphnode", │
│ "slug": "fortune", │
│ "path": "fetch", │
│ "target_fence": "python[execute=true]" │
│ } │
│ } │
└──────────────────────────────────────────────────────────────┘Result Contract
The result Variable
Python fences must store their output in a result variable:
# Your computation
data = fetch_something()
processed = transform(data)
# THE CONTRACT: Store output in `result`
result = {
"items": processed,
"count": len(processed),
"timestamp": datetime.now().isoformat()
}❌ Fence Execution Error: name 'fetch_something' is not defined Traceback (most recent call last): File "/app/oculus/providers/python_provider.py", line 468, in execute exec(fence_content, exec_globals, exec_locals) File "
", line 2, in NameError: name 'fetch_something' is not defined
Standard Result Structures
For tables (TableExtractor middleware):
result = {
"headers": ["Name", "Status", "Value"],
"rows": [
["Item1", "Active", 100],
["Item2", "Inactive", 0]
]
}For test suites:
result = {
"tests": [
{"test": "Test Name", "status": "PASS", "details": "..."},
{"test": "Test 2", "status": "FAIL", "details": "..."}
],
"summary": {
"total": 10,
"passed": 8,
"failed": 2,
"success_rate": "80%"
}
}Middleware System
TableConfig Middleware
Format results as markdown tables:
```graphnode:my-data-source:fetch
TableConfig:
array_path: items
columns:
Name: name
Status: status
Count: metrics.count
format: markdown
```Available Middleware
| Middleware | Pattern | Purpose |
|---|---|---|
DatetimeNormalizer |
* |
Convert datetime to ISO strings |
TableExtractor |
* (if TableConfig) |
Extract/format tables |
AWSResponseCleaner |
aws:* |
Strip ResponseMetadata |
FortuneFormatter |
graphnode:fortune:* |
Format fortune quotes |
XMLParser |
http:* |
Parse XML to dict |
SingleObjectFormatter |
* (if single object) |
Vertical key-value table |
Execution Context
FenceExecutionContext Fields
@dataclass
class FenceExecutionContext:
slug: str # Current node slug
mode: str # Cache level: raw/substituted/interpolated/rendered
registry: Registry # Node registry for resolving slugs
credentials: dict # AWS/API credentials
graph: dict # {'config': {...}} - injected parametersAccessing Context in Fences
{
"config": {},
"api_key": "default",
"base_url": "http://localhost:7778"
}Real-World Examples
Example 1: Test Data Provider
Node: poke-test-data
## config
```yaml
base_url: http://localhost:7778
test_node: poke-test-fixture
timeout: 5
```
## fetch
```python[execute=true]
import requests
BASE_URL = config.get('base_url')
tests = []
# Run tests...
r = requests.post(f"{BASE_URL}/api/oculus/poke/...")
tests.append({"test": "Prose Append", "status": "PASS"})
result = {"tests": tests, "summary": {...}}
```Consumer:
```graphnode:poke-test-data:table
TableConfig:
array_path: tests
columns:
Test: test
Status: status
format: markdown
```Example 2: Fortune Provider
Node: fortune
## config
```yaml
source: stuffy
count: 1
```
## fetch
```python[execute=true]
import requests
source = config.get('source', 'stuffy')
count = config.get('count', 1)
r = requests.get(f"http://localhost:8080/api/fortune?count={count}")
result = r.json()
```Consumer:
```graphnode:fortune:fetch
count: 3
```Anti-Patterns
❌ Don't: Forget the result variable
# BAD - no result variable
data = process()
print(data) # Lost!❌ Don't: Modify external state without care
# CAUTION - side effects
result = requests.post(...) # Creates/modifies data❌ Don't: Hardcode values that should be config
# BAD - hardcoded
url = "http://localhost:7778"
# GOOD - configurable
url = config.get('base_url', 'http://localhost:7778')✅ Do: Always provide sensible defaults
timeout = config.get('timeout', 5) # Default to 5 secondsImplementation Files
| File | Purpose |
|---|---|
fence_executor.py:2151-2347 |
GraphNodeProvider class |
middleware.py |
Middleware pipeline |
hierarchical_graph.py:29 |
FenceContent dataclass |
fence_metadata.py |
Dependency extraction |
Related Patterns
- [[pattern-embedded-tests]] - Embed tests as graphnodes
- [[pattern-fence-provider]] - Custom fence providers
- [[middleware-pipeline-pattern]] - Middleware architecture
- [[data-pipeline-pattern]] - Data transformation patterns
Slots
North
slots:
- patterns
- fence-architecture-patternSouth
slots:
- pattern-embedded-tests
- poke-test-data
- hierarchical-addressing-test-suite
- pattern-embedded-testsEast
slots:
- middleware-pipeline-pattern
- pattern-fence-provider
- middleware-pipeline-patternWest
slots:
- cmd-poke
- oculus-cli