Getting the haystack api keys environment setup right is the difference between a pipeline that runs and one that fails silently at 2 AM. This tutorial walks through configuring Haystack to work with n4n.ai’s OpenAI-compatible endpoint, covering environment management, component wiring, and a working RAG pipeline you can extend.
Prerequisites
Before starting, verify you have:
- Python 3.10 or higher
- An n4n.ai account with an API key (get one at n4n.ai if needed)
- Basic familiarity with Haystack 2.x concepts: components, pipelines, and document stores
Install the required packages:
pip install haystack-ai python-dotenv
The python-dotenv package keeps secrets out of source control. If you prefer another secrets manager (1Password CLI, Doppler, AWS Secrets Manager), adapt the pattern — the principle stays the same.
Project structure
Create a clean workspace:
mkdir haystack-n4n-tutorial
cd haystack-n4n-tutorial
Layout:
haystack-n4n-tutorial/
├── .env # never commit this
├── .env.example # commit this
├── requirements.txt
├── config.py # centralized configuration
├── pipeline.py # pipeline construction
└── run.py # entry point
Environment configuration
Create .env.example first — this documents required variables for teammates and CI:
# .env.example
N4N_API_KEY=your_key_here
N4N_BASE_URL=https://api.n4n.ai/v1
EMBEDDING_MODEL=text-embedding-3-small
GENERATION_MODEL=gpt-4o-mini
Now create the actual .env with your key:
# .env
N4N_API_KEY=sk-n4n-xxxxxxxxxxxxxxxx
N4N_BASE_URL=https://api.n4n.ai/v1
EMBEDDING_MODEL=text-embedding-3-small
GENERATION_MODEL=gpt-4o-mini
Checkpoint: Verify the file loads correctly:
# test_env.py
from dotenv import load_dotenv
import os
load_dotenv()
print("API key loaded:", bool(os.getenv("N4N_API_KEY")))
print("Base URL:", os.getenv("N4N_BASE_URL"))
Run it:
python test_env.py
Expected output:
API key loaded: True
Base URL: https://api.n4n.ai/v1
Centralized configuration module
Avoid scattering os.getenv() calls throughout your codebase. Create config.py:
# config.py
from dataclasses import dataclass
from dotenv import load_dotenv
import os
load_dotenv()
@dataclass(frozen=True)
class Settings:
n4n_api_key: str
n4n_base_url: str
embedding_model: str
generation_model: str
@classmethod
def from_env(cls) -> "Settings":
api_key = os.getenv("N4N_API_KEY")
base_url = os.getenv("N4N_BASE_URL", "https://api.n4n.ai/v1")
embedding_model = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small")
generation_model = os.getenv("GENERATION_MODEL", "gpt-4o-mini")
if not api_key:
raise ValueError("N4N_API_KEY not set in environment")
return cls(
n4n_api_key=api_key,
n4n_base_url=base_url.rstrip("/"),
embedding_model=embedding_model,
generation_model=generation_model,
)
settings = Settings.from_env()
Checkpoint: Test the config module:
python -c "from config import settings; print(settings.generation_model)"
Expected output:
gpt-4o-mini
Haystack components with n4n.ai
Haystack 2.x uses generators and embedders that accept an api_base_url parameter. This is where the n4n.ai integration happens — point the OpenAI-compatible clients at the n4n.ai endpoint.
Create pipeline.py:
# pipeline.py
from haystack import Pipeline
from haystack.components.generators import OpenAIGenerator
from haystack.components.embedders import OpenAITextEmbedder, OpenAIDocumentEmbedder
from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
from haystack.components.builders import PromptBuilder
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack import Document
from config import settings
def build_rag_pipeline() -> Pipeline:
document_store = InMemoryDocumentStore()
# Embedder for indexing documents
doc_embedder = OpenAIDocumentEmbedder(
api_key=settings.n4n_api_key,
api_base_url=settings.n4n_base_url,
model=settings.embedding_model,
)
# Embedder for query at runtime
text_embedder = OpenAITextEmbedder(
api_key=settings.n4n_api_key,
api_base_url=settings.n4n_base_url,
model=settings.embedding_model,
)
retriever = InMemoryEmbeddingRetriever(document_store=document_store, top_k=3)
prompt_template = """
Answer the question using only the provided context.
If the answer isn't in the context, say you don't know.
Context:
{% for doc in documents %}
{{ doc.content }}
{% endfor %}
Question: {{ question }}
Answer:
"""
prompt_builder = PromptBuilder(template=prompt_template)
generator = OpenAIGenerator(
api_key=settings.n4n_api_key,
api_base_url=settings.n4n_base_url,
model=settings.generation_model,
generation_kwargs={"temperature": 0.1, "max_tokens": 512},
)
pipeline = Pipeline()
pipeline.add_component("text_embedder", text_embedder)
pipeline.add_component("retriever", retriever)
pipeline.add_component("prompt_builder", prompt_builder)
pipeline.add_component("generator", generator)
pipeline.connect("text_embedder.embedding", "retriever.query_embedding")
pipeline.connect("retriever.documents", "prompt_builder.documents")
pipeline.connect("prompt_builder.prompt", "generator.prompt")
return pipeline, document_store, doc_embedder
Key points in this setup:
-
Explicit
api_base_url— Both embedders and the generator point tosettings.n4n_base_url. This is the single line that routes traffic through n4n.ai instead of directly to OpenAI. -
Model names pass through — The
modelparameter uses whatever you configured in.env. n4n.ai honors the model name and routes to the appropriate provider. -
In-memory document store — Keeps the tutorial self-contained. Swap for
WeaviateDocumentStore,QdrantDocumentStore, orPineconeDocumentStorein production.
Indexing sample documents
Create run.py to wire everything together:
# run.py
from pipeline import build_rag_pipeline
def main():
pipeline, document_store, doc_embedder = build_rag_pipeline()
# Sample documents — replace with your data source
docs = [
Document(content="Haystack 2.x uses a component-based architecture. Pipelines connect components via typed inputs and outputs."),
Document(content="The OpenAIGenerator accepts api_base_url to support OpenAI-compatible endpoints like n4n.ai."),
Document(content="InMemoryDocumentStore is suitable for prototypes. For production, use a vector database with persistence."),
Document(content="Environment variables should never be committed. Use .env.example to document required keys."),
]
print(f"Indexing {len(docs)} documents...")
doc_embedder.warm_up()
docs_with_embeddings = doc_embedder.run(docs)["documents"]
document_store.write_documents(docs_with_embeddings)
print("Indexing complete.")
# Test queries
questions = [
"What architecture does Haystack 2.x use?",
"How do I use n4n.ai with Haystack?",
"What document store should I use in production?",
]
for question in questions:
print(f"\n{'='*60}")
print(f"Q: {question}")
print(f"{'='*60}")
result = pipeline.run({
"text_embedder": {"text": question},
"prompt_builder": {"question": question},
})
answer = result["generator"]["replies"][0]
print(f"A: {answer.strip()}")
if __name__ == "__main__":
main()
Checkpoint: Run the pipeline:
python run.py
Expected output (answers will vary slightly by model):
Indexing 4 documents...
Indexing complete.
============================================================
Q: What architecture does Haystack 2.x use?
============================================================
A: Haystack 2.x uses a component-based architecture where pipelines connect components via typed inputs and outputs.
============================================================
Q: How do I use n4n.ai with Haystack?
============================================================
A: You use n4n.ai with Haystack by setting the api_base_url parameter on OpenAIGenerator and OpenAI embedders to https://api.n4n.ai/v1 and providing your n4n.ai API key.
============================================================
Q: What document store should I use in production?
============================================================
A: For production, use a vector database with persistence such as Weaviate, Qdrant, or Pinecone instead of InMemoryDocumentStore.
Verifying the request path
Confirm traffic actually goes through n4n.ai. Add a quick debug script:
# debug_request.py
import httpx
from config import settings
# Monkey-patch to log the request URL
original_post = httpx.Client.post
def logged_post(self, url, *args, **kwargs):
print(f"POST {url}")
return original_post(self, url, *args, **kwargs)
httpx.Client.post = logged_post
# Now run a minimal generation
from haystack.components.generators import OpenAIGenerator
gen = OpenAIGenerator(
api_key=settings.n4n_api_key,
api_base_url=settings.n4n_base_url,
model=settings.generation_model,
)
gen.warm_up()
result = gen.run("Say 'ok' if you receive this")
print(result["replies"][0])
Run it:
python debug_request.py
Expected output shows the n4n.ai base URL:
POST https://api.n4n.ai/v1/chat/completions
ok
If you see api.openai.com instead, check that api_base_url is set correctly on the generator.
Common issues and fixes
Authentication failures
openai.AuthenticationError: Invalid API key
Fix: Verify N4N_API_KEY in .env matches the key in your n4n.ai dashboard. No extra prefixes, no trailing whitespace.
Model not found
openai.NotFoundError: Model 'gpt-4o-mini' not found
Fix: The model name must exist in n4n.ai’s catalog. List available models via the n4n.ai dashboard or API. Update GENERATION_MODEL in .env accordingly.
Rate limits
openai.RateLimitError: Rate limit exceeded
n4n.ai handles provider-level fallbacks automatically, but client-side retry logic is still your responsibility. Add tenacity:
from tenacity import retry, stop_after_attempt, wait_exponential_jitter
@retry(
wait=wait_exponential_jitter(initial=1, max=10),
stop=stop_after_attempt(3),
)
def run_with_retry(pipeline, data):
return pipeline.run(data)
SSL certificate errors in corporate environments
If your MITM proxy intercepts TLS:
import os
os.environ["SSL_CERT_FILE"] = "/path/to/corporate-ca-bundle.crt"
Or disable verification for development only (never in production):
import httpx
httpx.Client(verify=False) # dev only
Production hardening checklist
Before deploying:
- Replace
InMemoryDocumentStorewith a persistent vector store - Move
.envloading to your platform’s secrets manager (AWS Parameter Store, GCP Secret Manager, Vault, etc.) - Add structured logging (structlog) and metrics (Prometheus) around pipeline runs
- Implement request/response logging for debugging — but redact PII and API keys
- Set up health checks that exercise the full pipeline, not just component imports
- Configure n4n.ai routing directives if you need specific provider preferences (e.g.,
{"provider": {"only": ["anthropic"]}})
Extending the pipeline
From here, typical additions:
- Hybrid retrieval: Combine
InMemoryBM25Retrieverwith the embedding retriever via aDocumentJoiner - Query rewriting: Add an
OpenAIGeneratorstep before embedding to expand or decompose the query - Citation extraction: Modify the prompt template to require
{{ doc.meta.source }}references - Streaming: Use
OpenAIGenerator(streaming_callback=...)for token-by-token UX
Each addition follows the same pattern: configure the component with api_base_url=settings.n4n_base_url and api_key=settings.n4n_api_key.
Summary
You now have a working Haystack RAG pipeline routed through n4n.ai with:
- Clean environment configuration via
.envand a typedSettingsclass - OpenAI-compatible components pointing at the n4n.ai endpoint
- A verified request path and debug technique
- A foundation ready for production hardening
The same pattern applies to any OpenAI-compatible gateway — swap the base URL and API key, and the rest of your Haystack code stays unchanged.