Skip to content

Output Formats & Integrations

indx can emit four output formats. This guide covers all four, how to select one, and why the .indx archive lets you move between frameworks freely.

The final stage of every build, 06 Embed+Pack, hands the assembled KnowledgeSpace to an OutputWriter, which decides what artifact lands on disk. You can choose the default portable .indx archive, plain JSONL, or objects ready to drop straight into a LangChain or LlamaIndex application.

OutputWriter is one of the six swappable component slots. Like every slot it is a typed Protocol, so any object that satisfies it can serialize a space. The built-in writers are resolved by name from the registry.

from pathlib import Path
from typing import Protocol, runtime_checkable
@runtime_checkable
class Writer(Protocol): # exported as `OutputWriter`
"""Serializes a KnowledgeSpace to a destination. Default: indx.
Also: jsonl, langchain, llamaindex."""
name: str
def write(self, space: KnowledgeSpace, dest: Path, *, name: str = "handbook") -> None: ...

The canonical class is Writer, also re-exported under the alias OutputWriter (from indx.output import OutputWriter). Two members make up the entire contract:

  • name — a string, the writer’s registry identifier, such as "indx".
  • write(space, dest, *, name="handbook") — materializes the space into the dest directory, using the keyword-only name as the archive base file name.

See the full slot list in the protocols reference.

--format nameWriter classEmitsAvailability
indx (default)IndxWriterPortable Zip archive (handbook.indx) + expanded layoutCore
jsonlJsonlWriterNewline-delimited documents/chunksCore (zero-dep)
langchainLangChainWriterLangChain Document objectsExtra: indx[langchain]
llamaindexLlamaIndexWriterLlamaIndex Node objectsExtra: indx[llamaindex]

The output writer is resolved with the same precedence as every slot: explicit code argument or use() → CLI flag → indx.toml → documented default. See the configuration guide for the full precedence rules.

Terminal window
# default: seal a portable .indx archive
indx ./docs --out ./ai-ready
# export newline-delimited JSONL instead
indx ./docs --out ./ai-ready --format jsonl
# emit LangChain Documents (requires the extra)
indx ./docs --out ./ai-ready --format langchain
[output]
format = ".indx" # ".indx" and "indx" both select the default archive writer

Pass output= as a name string or as a custom instance — see custom components.

from indx import DirectoryPipeline
# by name
pipeline = DirectoryPipeline(output="jsonl")
# or swap it later; use() accepts names or instances
pipeline = DirectoryPipeline().use(output="llamaindex")
space = pipeline.run("./docs", "./ai-ready")

The default IndxWriter seals the space into a single .indx file — a ZIP container (deflate) with a defined internal layout. That layout holds a manifest.json carrying checksums, the index.json knowledge graph, per-chunk files under chunks/, and the vector matrix under embeddings/.

Running a build also writes the expanded form alongside the archive, so downstream tools can read either shape:

ai-ready/
├── handbook.indx # the portable archive
├── index.json # the knowledge graph
├── chunks/ # agent-readable chunks + per-chunk context
└── embeddings/ # vectors + manifest

The defining property of .indx is that it is self-contained — you can re-load it without re-processing. The manifest pins the embedder name and dimensionality, for example the default openai:text-embedding-3-small, dim 1536. A consumer therefore knows exactly which model produced the vectors and can detect a mismatch before querying.

You can hand the file to anyone and reopen it instantly:

from indx import KnowledgeSpace
space = KnowledgeSpace.load("./ai-ready/handbook.indx")
hits = space.search("gdpr compliance", k=5)

Use --name to control the archive base name (handbookhandbook.indx). For the full byte-level layout, manifest schema, and versioning rules, see the .indx archive reference and the index.json reference.

The JsonlWriter emits newline-delimited records for documents and chunks. It ships in core and pulls no dependencies, and any tool can stream the output line by line.

This makes it a good fit for custom loaders, data warehouses, or quick scripts that do not need the sealed archive or memory-mapped vectors.

Framework writers — LangChain & LlamaIndex

Section titled “Framework writers — LangChain & LlamaIndex”

The framework writers skip the archive entirely. They hand you objects your existing application already understands:

  • langchain (LangChainWriter) emits LangChain Document objects, with page content plus metadata, ready to push into a LangChain retriever or vector store.
  • llamaindex (LlamaIndexWriter) emits LlamaIndex Node objects, preserving chunk text, source provenance, and relationships for a LlamaIndex index.

Both are optional extras. Install the matching extra before selecting one:

Terminal window
pip install "indx[langchain]"
# or
pip install "indx[llamaindex]"

If the extra is missing when the slot is selected, indx raises a single MissingDependencyError naming the exact pip install command. See errors & exit codes and the extras reference for the full matrix.

.indx (default)langchain / llamaindex
ArtifactSingle portable, checksummed fileNative LC Documents / LI Nodes
Re-loadable without re-processingYes — KnowledgeSpace.load(...)No — re-derive if you change stacks
Carries vectors + manifestYes (self-describing)Hands off to the framework’s own index
Best forArchiving, sharing, future-proofingPlugging straight into an existing LC/LI app

Reach for a framework writer when you already have a LangChain or LlamaIndex application and want indx’s structured chunks fed in with no glue code. Reach for .indx when you want a durable, neutral artifact that you can reopen, audit, and re-target later.

indx composes the AI stack rather than locking you into one. Every heavy capability sits behind a typed protocol with a named default: parser, LLM, VLM, embedder, store, and output.

This makes the .indx archive a neutral intermediate layer. It captures the expensive work once — walking, parsing, chunking, relating, enriching, and embedding — and the result is framework-agnostic.

That decoupling is what makes downstream migration cheap. The directory has already become a portable knowledge space, so switching the framework or stack on top of it is a configuration choice, not a re-derivation:

  • Build once into .indx, then export to LangChain today and LlamaIndex tomorrow by changing only [output].format — no re-walk, no re-parse, no re-embed.
  • Keep the canonical .indx as the source of truth and treat framework writers as disposable projections of it.
  • Re-embedding is only required if you change the embedder itself (the manifest pins the embedder’s name and dim); see reproducibility.

Need a format indx does not ship? Implement the Writer protocol (exported as OutputWriter) by setting a name string and a write(self, space, dest, *, name="handbook") method, then pass an instance to DirectoryPipeline(output=...) or use(output=...).

To distribute it for others to select by name, register it under the indx.outputs entry-point group. See custom components and authoring a plugin.