📚 Docs / Pipeline Layout JSON — spec for AI models & hand-editors

Pipeline Layout JSON — spec for AI models & hand-editors

This is the authoritative, machine-actionable spec for the pipeline-editor layout JSON. It is written so an AI model (or a human) can produce a valid file from scratch that the editor will import without errors.

1. File envelope (two accepted shapes)

The import function parsePipelineLayout(text) accepts either:

jsonc// Shape A — versioned export (what the editor's ↓ button writes)
{
  "version": 1,
  "nodes": [ /* Node[] */ ],
  "edges": [ /* Edge[] */ ]
}
jsonc// Shape B — bare layout (also valid; version is optional)
{
  "nodes": [ /* Node[] */ ],
  "edges": [ /* Edge[] */ ]
}

If you author a file, prefer Shape A with "version": 1 (matching serializeLayout). version is informational — the parser does not check it.


2. Node object schema

jsonc{
  "id": "dense",                        // string, REQUIRED, must be unique across nodes
  "type": "denseRetrieval",             // string, REQUIRED, must be in KNOWN_TYPES (see §3)
  "position": { "x": 280, "y": 440 },   // {x,y} numbers, REQUIRED (any values allowed)
  "data": {
    "label": "Dense Retrieval",         // string, recommended (shown on the node)
    "status": "idle",                   // optional, one of idle|active|done|error; runtime-only
    "files": ["a.pdf"],                 // optional, ingest node only
    "icon": "🔧",                       // optional, tool node only
    "description": "..."                // optional, tool node only
  }
}

Rules enforced by validateLayout:


3. Valid node types (KNOWN_TYPES)

typeCanonical default idDefault positionMeaning
intentClassifierintent{450, 0}Classifies greeting vs legal query
chitchatchitchat{100, 220}Greeting reply, no retrieval
plannerplanner{450, 220}Writes search plan, dispatches to dense + sparse
denseRetrievaldense{280, 440}Semantic search (ChromaDB)
sparseRetrievalsparse{620, 440}Keyword search (BM25)
rerankrerank{450, 660}Cross-encoder re-scoring
ingestingest{100, 440}Parse attached files → chunks → embed → store
scanscan{100, 660}Playbook compliance scan + flags
answeranswer{450, 880}Final answer from retrieved docs
tool(any)(any)Generic custom node (drag-and-drop)

The five toggleable sub-nodes (palette switches) are: denseRetrieval, sparseRetrieval, rerank, ingest, scan — mapped to canonical ids dense, sparse, rerank, ingest, scan. Keep those ids if you want the toggle switches to control the node.


4. Edge object schema

jsonc{
  "id": "dense-rerank",                 // string, REQUIRED, must be unique across edges
  "type": "particle",                   // string; 'particle' or 'default' kept, anything else coerced to 'particle'
  "source": "dense",                    // string, REQUIRED — must exist in nodes, else edge dropped
  "target": "rerank",                   // string, REQUIRED — must exist in nodes, else edge dropped
  "sourceHandle": "chitchat",           // optional — named output handle (see §5)
  "targetHandle": null,                 // optional — usually omitted
  "label": "candidates",                // optional — shown on the edge
  "style": { "stroke": "#8b5cf6", "strokeWidth": 2 },           // optional
  "labelStyle": { "fill": "#8b5cf6", "fontSize": 11, "fontWeight": 600 },  // optional
  "labelBgStyle": { "fill": "var(--bg-primary)", "fillOpacity": 0.9 },     // optional
  "data": {
    "color": "#8b5cf6",                 // optional — used by the particle animation
    "active": false,                    // optional — runtime-only, stripped on export
    "dataType": "candidates",           // optional — shown in live payload preview
    "payloadPreview": "top 50 dense"    // optional — shown in live payload preview
  }
}

Rules enforced by validateLayout:


5. Named handles (important for the intent node)

Only the intent node exposes named source handles. If an edge leaves intent, set sourceHandle to one of:

sourceHandleMeaningUsed by default edge
chitchatgreeting routeintent-chitchat
plannerlegal-query routeintent-planner
ingestattachment routeintent-ingest

All other nodes use a single unnamed source/target handle — omit sourceHandle/targetHandle for them.


6. Bypass edges (what a removed sub-node looks like)

When a middle sub-node is removed, the editor rewires incoming edges straight to outgoing targets. Those rewires are tagged so re-enabling can find them:

Example — the reranker removed:

jsonc{ "id": "bypass-rerank-dense-answer-0-0-1750000000000", "type": "particle",
  "source": "dense", "target": "answer", "label": "bypass",
  "data": { "color": "var(--accent)", "active": false, "dataType": "data",
  "payloadPreview": "bypassed step" } }
{ "id": "bypass-rerank-sparse-answer-1-0-1750000000000", "type": "particle",
  "source": "sparse", "target": "answer", "label": "bypass",
  "data": { "color": "var(--accent)", "active": false, "dataType": "data",
  "payloadPreview": "bypassed step" } }

7. A complete valid example (dense + sparse → answer, no rerank, no scan)

This file is guaranteed to import cleanly — it references only known node types, unique ids, and edges whose endpoints exist:

json{
  "version": 1,
  "nodes": [
    { "id": "intent", "type": "intentClassifier", "position": { "x": 450, "y": 0 }, "data": { "label": "Intent Classifier" } },
    { "id": "planner", "type": "planner", "position": { "x": 450, "y": 220 }, "data": { "label": "Planner" } },
    { "id": "dense", "type": "denseRetrieval", "position": { "x": 280, "y": 440 }, "data": { "label": "Dense Retrieval" } },
    { "id": "sparse", "type": "sparseRetrieval", "position": { "x": 620, "y": 440 }, "data": { "label": "Sparse Retrieval" } },
    { "id": "answer", "type": "answer", "position": { "x": 450, "y": 880 }, "data": { "label": "Answer" } }
  ],
  "edges": [
    { "id": "intent-planner", "type": "particle", "source": "intent", "target": "planner", "sourceHandle": "planner",
      "label": "legal query", "style": { "stroke": "#f59e0b", "strokeWidth": 2 },
      "data": { "color": "#f59e0b", "active": false, "dataType": "user query", "payloadPreview": "What is rescission?" } },
    { "id": "planner-dense", "type": "particle", "source": "planner", "target": "dense", "label": "query",
      "style": { "stroke": "#8b5cf6", "strokeWidth": 2 },
      "data": { "color": "#8b5cf6", "active": false, "dataType": "query", "payloadPreview": "dense search on embeddings" } },
    { "id": "planner-sparse", "type": "particle", "source": "planner", "target": "sparse", "label": "query",
      "style": { "stroke": "#8b5cf6", "strokeWidth": 2 },
      "data": { "color": "#8b5cf6", "active": false, "dataType": "query", "payloadPreview": "BM25 keyword search" } },
    { "id": "bypass-rerank-dense-answer-0-0-1750000000000", "type": "particle", "source": "dense", "target": "answer",
      "label": "bypass", "style": { "stroke": "var(--accent)", "strokeWidth": 2 },
      "data": { "color": "var(--accent)", "active": false, "dataType": "data", "payloadPreview": "bypassed step" } },
    { "id": "bypass-rerank-sparse-answer-1-0-1750000000000", "type": "particle", "source": "sparse", "target": "answer",
      "label": "bypass", "style": { "stroke": "var(--accent)", "strokeWidth": 2 },
      "data": { "color": "var(--accent)", "active": false, "dataType": "data", "payloadPreview": "bypassed step" } }
  ]
}
Note: bypass-rerank-… ids are arbitrary here — the editor only matches them by the bypass-<nodeId>- prefix when re-enabling the reranker. Using the real pattern keeps re-enable behavior correct.

8. Generation checklist (AI must follow all of these)

  1. Envelope: { "version": 1, "nodes": [...], "edges": [...] }.
  2. Every node: has string id (unique), valid type from §3, and a position object with numeric x/y. Recommended: data.label.
  3. Every edge: has string id (unique), source and target that both exist in the node list, and type: "particle".
  4. No dangling edges: an edge to a node you deleted is invalid — it will be silently dropped.
  5. No unknown types: using searchDocuments, scanIngest, or any other non-§3 type gets the node silently dropped. This is the most common AI mistake.
  6. Named handles only from intent (§5); omit them elsewhere.
  7. Zero nodes is legal (a deliberately emptied canvas round-trips), but a file with edges and no valid nodes is pointless — edges get dropped.
  8. Keep ids stable and semantic (dense, sparse, rerank, ingest, scan, planner, answer, intent, chitchat) so palette toggles and re-enabling work.
  9. Extra fields are tolerated: the parser ignores unknown properties.

9. Round-trip guarantee

Export writes {version, nodes, edges} with runtime fields stripped (status, active, selected). Import accepts that exact output back — export → import is identity-preserving. If you want to edit an exported file: change positions, delete nodes and every edge touching them, and keep all other fields as-is.


10. Backend runtime configuration (POST /api/pipeline/config)

The backend validates and consumes exactly the same JSON this spec covers. The editor no longer needs to be visualization-only: sending the layout to the backend reconfigures retrieval for every subsequent query.

Endpoint

Optional query overrides on POST (win over the node-derived values): top_k (1–100), top_k_retrieval (1–500), dense_weight, sparse_weight (0–10).

bashcurl -X POST http://localhost:8765/api/pipeline/config \
  -H "Content-Type: application/json" \
  -d @pipeline-layout.json \
  "?top_k=8&dense_weight=0.7&sparse_weight=0.3"

Derivation rules (from which sub-nodes are present)

Sub-node stateskip_reranksearch_modeweights
rerank presentFalse (rerank ON)
rerank absentTrue (rerank OFF)
denseRetrieval + sparseRetrievalhybriddense_w / sparse_w (default 1/1)
only denseRetrievaldensesparse_w forced to 0
only sparseRetrievalsparsedense_w forced to 0
neither retrieval nodefile default

Precedence: file defaults < layout config < explicit per-request params (skip_rerank, top_k in the chat request). One exception: a layout with an explicit top_k override wins over the chat UI's fixed default of 5 (the frontend sends top_k: 5 unconditionally).

Behavior notes

Questions, answered

Short, self-contained answers about this guide.

What does a pipeline layout JSON look like?

A JSON object with nodes (each with an id, type, and sub-node state) and edges connecting them. The doc lists every field with examples — including canonical sub-node IDs and per-node config overrides like top_k.

How is the layout validated?

The layout is validated before save and before applying to the backend — unknown node types, dangling edges, and invalid overrides are rejected so the runtime never receives a malformed graph.

Where is the source of truth?

pipelineStorage.ts in the frontend is the canonical implementation; docs/PIPELINE_JSON.md documents the schema. The pipeline config endpoint maps the JSON onto backend flags like skip_rerank and search_mode.