When a Haystack pipeline fails silently or returns bad results, here is how to find exactly which component is the problem.

Why Haystack Pipelines Are Hard to Debug

Haystack pipelines are declarative: you connect components and run the pipeline, and data flows through them automatically. When something goes wrong -- a component returns empty results, the wrong documents get retrieved, the LLM produces a bad answer -- it is not always obvious which component is the culprit or what its inputs were.

This article covers five techniques to make Haystack pipelines fully transparent.

Technique 1: Run Components Individually

Before connecting components into a pipeline, test each one in isolation. This is the fastest way to verify component behaviour before debugging a full pipeline.

from haystack.components.retrievers import InMemoryBM25Retriever
from haystack.document_stores.in_memory import InMemoryDocumentStore
 
# Test the retriever directly
store = InMemoryDocumentStore()
store.write_documents([
    Document(content="The refund policy allows 30 days."),
    Document(content="Shipping takes 3-5 business days."),
])
 
retriever = InMemoryBM25Retriever(document_store=store)
result = retriever.run(query="refund policy", top_k=2)
 
# Inspect the output before connecting to a generator
print(f"Retrieved {len(result['documents'])} documents")
for doc in result['documents']:
    print(f"  Score: {doc.score:.3f} | Content: {doc.content[:100]}")

Technique 2: Capture Intermediate Outputs

When you call pipeline.run(), the return value only contains outputs from the final component (or components with no outgoing connections). To inspect intermediate outputs, add your target component to the include_outputs_from parameter.

pipeline = Pipeline()
pipeline.add_component("retriever", retriever)
pipeline.add_component("ranker", ranker)
pipeline.add_component("generator", generator)
pipeline.connect("retriever.documents", "ranker.documents")
pipeline.connect("ranker.documents", "generator.documents")
 
# Standard run -- only returns generator output
result = pipeline.run({"retriever": {"query": "refund policy"}})
 
# Debug run -- returns outputs from ALL listed components
result = pipeline.run(
    {"retriever": {"query": "refund policy"}},
    include_outputs_from={"retriever", "ranker", "generator"},
)
 
# Now inspect every stage
print("Retriever output:", len(result["retriever"]["documents"]), "docs")
print("Ranker output:", len(result["ranker"]["documents"]), "docs")
print("Generator output:", result["generator"]["replies"][0])
include_outputs_from is your most powerful debugging tool in Haystack. Use it any time you need to see what is happening inside a pipeline without modifying your component code.

Technique 3: Enable Pipeline Logging

Haystack uses Python's standard logging module. Enabling DEBUG level logging shows every component's execution, its inputs, and its outputs in real time.

import logging
 
# Enable Haystack debug logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("haystack").setLevel(logging.DEBUG)
 
# Now run your pipeline -- you will see detailed logs for every component execution
result = pipeline.run({"retriever": {"query": "test query"}})
 
# For less noise, enable only for specific components:
logging.getLogger("haystack.components.retrievers").setLevel(logging.DEBUG)
logging.getLogger("haystack.components.generators").setLevel(logging.WARNING)

Technique 4: Add a Passthrough Debug Component

Insert a simple logging component anywhere in your pipeline to inspect data as it flows through. Remove it before production.

from haystack import component, Document
from typing import List, Any
 
@component
class DebugPassthrough:
    """Logs pipeline data at a specific point. Remove before production."""
 
    def __init__(self, label: str = "DEBUG"):
        self.label = label
 
    @component.output_types(documents=List[Document])
    def run(self, documents: List[Document]) -> dict:
        print(f"\n=== {self.label} ===")
        print(f"Document count: {len(documents)}")
        for i, doc in enumerate(documents[:3]):  # show first 3
            print(f"  [{i}] score={getattr(doc, 'score', 'N/A'):.3f} "
                  f"content={doc.content[:100]!r}")
        print("=" * (len(self.label) + 8))
        return {"documents": documents}  # pass through unchanged
 
# Insert between retriever and ranker
pipeline.add_component("debug_after_retrieval", DebugPassthrough("POST-RETRIEVAL"))
pipeline.connect("retriever.documents", "debug_after_retrieval.documents")
pipeline.connect("debug_after_retrieval.documents", "ranker.documents")

Technique 5: Validate Pipeline Connections

Connection errors in Haystack (wrong types, missing inputs, disconnected components) surface at run time rather than definition time. Call pipeline.show() or pipeline.to_dict() to inspect the connection graph before running.

# Visualise the pipeline (requires graphviz)
pipeline.show()  # opens a visual diagram
 
# Or get a text summary of all components and their connections
import json
pipeline_dict = pipeline.to_dict()
for component_name, component_data in pipeline_dict["components"].items():
    print(f"Component: {component_name}")
    print(f"  Type: {component_data['type']}")
 
# Validate that all required inputs are connected
# (Haystack raises PipelineConnectError for type mismatches)
try:
    pipeline.run({"retriever": {"query": "test"}})
except Exception as e:
    print(f"Pipeline error: {type(e).__name__}: {e}")

Common Failure Patterns

Symptom Likely cause Fix
Empty documents returned Retriever finds no matches -- query or content mismatch Test retriever in isolation; check document store has content
LLM returns generic answer Retriever results not reaching the generator Use include_outputs_from to check intermediate outputs
Pipeline hangs silently Component waiting for an input that is never provided Check all required inputs are connected; use pipeline.show()
Type error at connection Output type of one component does not match input type of next Check @component.output_types matches the downstream input annotation
Component skipped entirely Optional input is None and component has no None handling Add None guard in run() or make the input required

Quick Reference

  • Test components individually via component.run() before wiring into a pipeline
  • Use include_outputs_from={'component_name'} to capture intermediate outputs
  • Enable logging.getLogger('haystack').setLevel(logging.DEBUG) for full execution trace
  • Add a DebugPassthrough component to inspect data at any pipeline point
  • Call pipeline.show() to visualise connections before running