Skip to content

Configuring indx (indx.toml)

indx.toml is an optional file that pins which components your pipeline uses and how they behave. You never need it — the documented defaults make a full run work on their own.

Reach for a config file when you want a build to be reproducible, shareable, and explicit about its stack.

Every key in indx.toml has a documented default, so a bare run resolves a complete stack on its own:

Terminal window
indx ./docs --out ./ai-ready

That run resolves the full default stack without any config file (once the selected extras are installed):

  • Parser docling
  • LLM openai:gpt-5-mini, VLM none
  • Embedder openai:text-embedding-3-small
  • Store qdrant
  • Output .indx

A config file lets you write those choices down, override individual slots, and pass backend-specific options that the CLI flags don’t cover.

For each component slot, indx resolves the effective value from four layers, highest priority first:

explicit code argument / use() > CLI flag > indx.toml > documented default
LayerExampleWins over
Explicit code arg / use()DirectoryPipeline(store="chroma") or .use(store="chroma")everything below
CLI flagindx ./docs -o ./out --store chromaindx.toml and the default
indx.toml[store] backend = "chroma"the documented default
Documented defaultstore = "qdrant"

A name that doesn’t resolve to a registered component is a fatal error in any layer. indx raises it before any stage runs, so a bad config fails fast rather than mid-build.

A bad config file or unknown component name exits with code 3. See errors and exit codes.

Every section and key below is optional; omitted keys fall back to the documented default. The sections map one-to-one onto the pipeline’s component slots.

[parser]
engine = "docling" # str. Parser name. Default: "docling".
[enrich]
llm = "openai:gpt-5-mini" # str. LLM name[:model] or "none". Default: "openai:gpt-5-mini".
vlm = "none" # str. VLM name or "none". Default: "none".
metadata = ["type", "topics", "tags", "summary"]
# list[str]. Which enrichments to produce.
# Default: ["type", "topics", "tags", "summary"].
[embed]
model = "openai:text-embedding-3-small" # str. Embedder name. Default cloud embedder.
[store]
backend = "qdrant" # str. One of: qdrant | pgvector | chroma | lancedb | jsonl.
# Default: "qdrant".
[output]
format = ".indx" # str. One of: .indx | indx | jsonl | langchain | llamaindex.
# Default: ".indx" (writer registry name: "indx").
SectionKeyTypeDefaultAllowed values
[parser]enginestringdoclingany registered parser name
[enrich]llmstringopenai:gpt-5-mini<name>[:model], none
[enrich]vlmstringnone<name>, none
[enrich]metadatalist[str]["type","topics","tags","summary"]subset of those four
[embed]modelstringopenai:text-embedding-3-smallany registered embedder name
[store]backendstringqdrantqdrant, pgvector, chroma, lancedb, jsonl
[output]formatstring.indx.indx, indx, jsonl, langchain, llamaindex

For the exhaustive table with every key, type, and constraint, see the configuration reference.

Secrets come from the environment, never the file

Section titled “Secrets come from the environment, never the file”

indx.toml is meant to be committed and shared, so it must not contain credentials. Supply API keys and other secrets through environment variables instead.

pydantic-settings layers them in at runtime, so they are never written into the file.

Terminal window
export INDX_LLM__API_KEY="sk-..." # double underscore = nested setting
indx ./docs --out ./ai-ready --llm openai:gpt-5-mini
[enrich]
llm = "openai:gpt-5-mini" # the model choice lives here…
# …the api_key comes from $INDX_LLM__API_KEY, not this file
# pick a different model with the :model suffix, e.g. "openai:gpt-4o"

The env prefix is keyed on the slot name, not the TOML section heading. So the LLM’s key is INDX_LLM__… even though the LLM is configured under [enrich].

The double underscore separates the slot from the nested setting, giving the form INDX_<SLOT>__<SETTING>:

SlotTOML locationEnv prefix
parser[parser] engineINDX_PARSER__…
llm[enrich] llmINDX_LLM__…
vlm[enrich] vlmINDX_VLM__…
embedder[embed] modelINDX_EMBEDDER__…
store[store] backendINDX_STORE__…

Most adapters accept options beyond the simple slot name. Those live in a sub-table named after the backend, such as [store.qdrant].

indx passes the keys in that sub-table verbatim to the adapter constructor. The core treats them as opaque, so each backend documents its own keys.

[store]
backend = "qdrant"
[store.qdrant]
url = "http://localhost:6333" # passed straight through to the Qdrant adapter
# collection = "handbook" # any further keys are adapter-defined

The same pattern applies to other slots and to third-party plugins. Once a plugin is installed, its name works in backend/engine/etc. and its sub-table carries its options. See Bring your own stack and authoring a plugin.

If you don’t pass --config, indx auto-loads ./indx.toml from the current directory when it exists. To use a different file, point the CLI or SDK at it explicitly:

Terminal window
indx ./docs --out ./ai-ready --config ./configs/prod.indx.toml
from indx import DirectoryPipeline
space = DirectoryPipeline(config="./configs/prod.indx.toml").run("./docs", "./ai-ready")

In the SDK, config accepts either a path string or a Config object (from indx.config import Config). Component arguments passed to the constructor or use() still override anything the file says, following the precedence rules above.

The resolved stack is recorded for reproducibility

Section titled “The resolved stack is recorded for reproducibility”

After all four layers are merged, indx records the resolved component names in a manifest that is written into both index.json (under metadata) and the archive’s manifest.json.

The manifest pins the chosen parser, llm, vlm, embedder, and store (under the components map), plus the embedding_model and embedding_dim actually used. This makes a .indx archive self-describing, so a build stays auditable long after the fact.

{
"metadata": {
"schema_version": "1",
"indx_version": "0.4.2",
"source_root": "/path/to/docs",
"components": {
"parser": "docling",
"llm": "openai:gpt-5-mini",
"vlm": "none",
"embedder": "openai:text-embedding-3-small",
"store": "qdrant"
},
"embedding_model": "openai:text-embedding-3-small",
"embedding_dim": 1536
}
}

Combined with deterministic ids and temperature=0.0 enrichment, this is what makes a re-run over unchanged input reproducible. See Reproducibility for the full guarantees.

Why TOML, and why indx never rewrites your config

Section titled “Why TOML, and why indx never rewrites your config”

indx parses indx.toml with the stdlib tomllib. It is available since Python 3.11, which is indx’s floor, so no extra parsing dependency is needed.

tomllib is read-only: it can parse TOML but cannot write it. indx never re-serializes your file — it only ever reads the config you author.

The upshot: indx never writes indx.toml at all, so your comments and formatting are never clobbered.