Skip to content

Building a Custom Dashboard

HEBBS ships with a built-in Memory Palace UI (hebbs panel), but you can build your own dashboard using the REST API on port 6381. This cookbook covers the exploration and graph endpoints that make custom UIs possible.

  • A running HEBBS server (see Getting Started)
  • An API key (printed on first start, or set HEBBS_AUTH_ENABLED=false for local dev)
  • Some memories already stored (run a few remember calls first)

The REST API exposes five endpoints specifically designed for exploration and visualization:

EndpointPurpose
GET /v1/memoriesBrowse and paginate all memories
GET /v1/memories/:id/edgesGet the graph connections for a node
POST /v1/memories/batchFetch many memories in one request
POST /v1/graphQuery a subgraph for visualization
GET /v1/entitiesList all entities for navigation

These complement the existing recall, prime, and insights endpoints. See the full REST Endpoints reference for details.

Start by fetching all entities to populate a sidebar or dropdown.

Terminal window
curl -s http://localhost:6381/v1/entities \
-H "Authorization: Bearer $HEBBS_API_KEY" | jq .

Use cursor-based pagination to load memories incrementally. Filter by entity or kind.

Terminal window
# First page
curl -s "http://localhost:6381/v1/memories?entity_id=customer-42&kind=episode&limit=50" \
-H "Authorization: Bearer $HEBBS_API_KEY" | jq .
# Next page (use next_cursor from previous response)
curl -s "http://localhost:6381/v1/memories?entity_id=customer-42&cursor=01KK3S0K...&limit=50" \
-H "Authorization: Bearer $HEBBS_API_KEY" | jq .

Available kind values: episode, insight, revision, document, proposition.

The /v1/graph endpoint returns a subgraph centered on any node, with all nodes and edges needed for visualization.

Terminal window
curl -s -X POST http://localhost:6381/v1/graph \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $HEBBS_API_KEY" \
-d '{
"center_id": "01KK3S0KXYE22EFF489YRTH559",
"depth": 2,
"max_nodes": 100
}' | jq .

The response contains everything needed to render a graph visualization:

  • nodes: Full Memory objects (content, importance, kind, timestamps, etc.)
  • edges: Typed, directed connections between nodes with confidence scores
  • truncated: Whether the traversal was cut short (useful for showing a “load more” button)
ParameterDefaultMaxDescription
depth25How many hops from the center node
max_nodes100500Upper bound on returned nodes
edge_typesallRestrict traversal to specific edge types

When a user clicks a node, load its full edge details.

const res = await fetch(
`http://localhost:6381/v1/memories/${memoryId}/edges`,
{ headers: { Authorization: `Bearer ${API_KEY}` } }
);
const { outgoing, incoming } = await res.json();
// outgoing: edges FROM this memory to others
// incoming: edges FROM other memories TO this one
// Each edge: { source_id, target_id, edge_type, confidence, timestamp_us }

When rendering a graph or table, use batch get to fetch all memories in one request instead of N individual calls.

const res = await fetch("http://localhost:6381/v1/memories/batch", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
memory_ids: ["01KK3S0K...", "01KK3T1M...", "01KK3U2N..."],
}),
});
const memories = await res.json();

Batch get accepts up to 500 IDs per request. Missing IDs are silently skipped.

Subscribe to memory changes to keep your dashboard in sync without polling.

const eventSource = new EventSource(
"http://localhost:6381/v1/subscribe",
// Note: SSE via POST requires a fetch-based EventSource polyfill
);
// Or use fetch with ReadableStream:
const res = await fetch("http://localhost:6381/v1/subscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
entity_id: "customer-42",
confidence_threshold: 0.5,
}),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
// Parse SSE "data:" lines and update the UI
for (const line of text.split("\n")) {
if (line.startsWith("data:")) {
const event = JSON.parse(line.slice(5));
// event.memory contains the new/updated Memory object
addNodeToGraph(event.memory);
}
}
}

A typical dashboard data flow:

  1. On load: GET /v1/entities to populate entity selector
  2. On entity select: GET /v1/memories?entity_id=...&limit=50 to list memories
  3. On memory select: POST /v1/graph to render the neighborhood
  4. On node click: GET /v1/memories/:id/edges for edge details
  5. Background: POST /v1/subscribe for live updates
  6. Search: POST /v1/recall with similarity strategy for semantic search
  7. Insights panel: GET /v1/insights?entity_id=... for consolidated knowledge
Edge TypeMeaningVisual Suggestion
caused_byA caused BSolid arrow
related_toSemantic relationshipDashed line
followed_byTemporal sequenceThin arrow
revised_fromB is a revision of ADotted arrow
insight_fromInsight derived from episodesGlowing edge
contradictsMemories conflictRed edge
has_entityMemory mentions an entityGray edge
entity_relationRelationship between entitiesBlue edge
proposition_ofAtomic fact extracted from a documentPurple edge
KindDescriptionVisual Suggestion
episodeRaw experience or observationDefault node
insightConsolidated knowledge from reflectionStar/diamond node
revisionUpdated version of a prior memoryOutlined node
documentWhole-file or LLM-summarized contentDocument icon
propositionAtomic fact extracted by LLMSmall dot