Skip to content

The Pipeline & Stages

DirectoryPipeline is the engine that turns a directory into a KnowledgeSpace. It is an ordered list of stages you can inspect, extend, and trim. The CLI, SDK, and archive format all expose the same pipeline model.

A pipeline is an ordered set of stages. Each stage receives and returns one shared, mutable object: the SpaceContext. Every stage obeys the same contract:

def run(self, ctx: SpaceContext) -> SpaceContext: ...

A stage returns the same SpaceContext instance it received, mutated in place. Stages never communicate through globals or side channels, only through the context.

That single rule gives every stage a clear view of the work done so far, and makes insertion, replacement, and removal predictable.

A fresh DirectoryPipeline registers six built-in stages in canonical order. Each stage drives a component slot, or is fully built-in:

#Stage (name)One-line responsibilityComponent slot
01walkTraverse the folder/ZIP, build the directory graph, detect file types— (built-in)
02parseRun each file through the chosen parser → text, tables, layout, imagesParser
03chunkSplit content with structure intact; keep lineage and neighbor links— (built-in)
04relateResolve references, siblings, parents, duplicates → typed Relations— (built-in)
05enrichLLM/VLM add detected type, topics, tags, and summariesLLM, VLM
06embed-packVectorize chunks, write to the store, seal the .indx archiveEmbedder, Store, OutputWriter

For a deep dive into the inputs, outputs, and tuning of each stage, see the pipeline reference and the per-stage pages: Walk, Parse, Chunk, Relate, Enrich, and Embed+Pack.

The SpaceContext carries the inputs (root, out, config), the resolved components, and a set of collections that fill up phase by phase. Each stage appends to the collection relevant to its work. Later stages read what earlier stages produced:

01 Walk → ctx.space.documents_ (one Document per file, with lineage + detected type)
02 Parse → ctx.parsed (doc_id → ParsedDoc)
03 Chunk → ctx.space.chunks (retrievable units, with prev_id/next_id links)
04 Relate → ctx.space.relations (typed edges between docs/chunks)
05 Enrich → enriches ctx.space.documents_ (adds doc_type, topics, tags, summary)
06 Embed+Pack → sets Chunk.embedding on ctx.space.chunks + sealed .indx manifest

The document list (ctx.space.documents_) is built up across stages 01–05, and chunk vectors land last. The context already holds a live KnowledgeSpace (ctx.space) that the stages fill in place:

documents_ → parsed → chunks → relations → chunk embeddings ⇒ ctx.space (KnowledgeSpace)

That arrow diagram shows how collections nest inside ctx.space (a KnowledgeSpace). It is not a second execution order. Stages still run 01–06, and ctx.space.documents_ is refined in place across stages 01–05 rather than created in one step.

Per-item failures collected along the way are recorded on ctx.errors and surfaced on the result under space.metadata["errors"].

Every stage has a stable name: walk, parse, chunk, relate, enrich, embed-pack. Because stages are an ordered list keyed by name, you can reshape the pipeline with a small API on DirectoryPipeline:

MethodWhat it does
stages()Return the current ordered stage list.
insert(index, stage)Insert a custom stage at a 0-based position.
append(stage)Add a stage to the end.
replace(name, stage)Swap out the stage with the given name.
drop(name)Remove the named stage entirely.

These return the pipeline for chaining. Components, not stages, are swapped separately with use(parser=..., llm=..., store=...). See Bring your own stack.

from indx import DirectoryPipeline
# PiiRedactStage and MyChunker are user-defined — see /guides/custom-stage/.
pipeline = (
DirectoryPipeline(embedder="bge-m3", store="chroma")
.drop("enrich") # skip all LLM work
.insert(3, PiiRedactStage()) # after Chunk, before Relate and Enrich
)
space = pipeline.run("./notes", "./out")
  • drop("enrich") — skip LLM/VLM work entirely. A fully supported, common operation. Useful when no model is available, or when you only need the structural graph of folders, neighbors, and relations without topics, tags, or summaries.
  • drop("embed-pack") — produce a graph-only space with no vectors. Helps when a downstream store self-embeds, or when you want to inspect structure before committing to an embedder. The CLI exposes the same intent as --no-embed.
  • insert(i, stage) — add a custom pass. For example, redaction before Enrich so sensitive content is stripped before any egress-capable component sees it, or a deduplication step before Relate.
  • replace("chunk", MyChunker()) — substitute a built-in stage with your own implementation.

Not every failure should stop a build. indx distinguishes two kinds:

  • Per-item (skip) — a single file fails to parse, or one document’s LLM call times out. The item is skipped, a skip-kind error is appended to ctx.errors, and the pipeline continues. This is the default behaviour for Parse and Enrich.
  • Fatal — misconfiguration, an unreachable store, an unresolvable component name, or a stage raising an unhandled exception. The pipeline aborts and re-raises a wrapping error.

The --strict CLI flag, and strict=True in the SDK, promotes every skip into a fatal error, so any single failure aborts the run.

Either way, errors are visible. Nothing is silently swallowed, and ctx.errors ends up on space.metadata["errors"] for inspection.

Because the pipeline is an ordered list and every stage shares one typed contract, the same code can run on a laptop, CI worker, or air-gapped server. Swap cloud components for local ones without rewriting orchestration.

Stages stay replaceable, components stay selectable by name or object, and the output remains deterministic and portable.