skip to content

In Haystack, what does Pipeline.connect() validate, and when does it fail?

level: middleimportance: must knowfreq 75%

answer

  1. Errors surface when you build, not run
  2. Sockets carry names and types
  3. Sender output type must fit receiver input
  4. Socket names optional only when unambiguous
  5. Non-variadic input takes one connection

basics

~20 s

Pipeline.connect() checks at wiring time that both named sockets exist and that the sender's declared output type is compatible with the receiver's input type. A mismatch raises immediately, so a broken graph fails before any model is called.

solid answer

~40 s

`connect("sender.socket", "receiver.socket")` resolves four things and refuses the edge if any fails: both components are in the pipeline, the named output socket exists on the sender, the named input socket exists on the receiver, and the sender's output type is accepted by the receiver's input type. The types come from the component's declared output types and its `run()` parameter annotations, so wiring `List[Document]` into a `str` input is rejected at `connect()` — not five minutes into a run. You can drop the socket names (`connect("retriever", "prompt")`) when exactly one compatible pair exists; if there are zero or several, Haystack raises and tells you the candidates. `connect()` returns the pipeline, so calls chain. The practical payoff is that a whole class of integration bugs becomes an import-time error in CI rather than a production traceback.

code

python · 14 lines
python
from haystack import Document, Pipeline
from haystack.components.builders import PromptBuilder
from haystack.components.retrievers.in_memory import InMemoryBM25Retriever
from haystack.document_stores.in_memory import InMemoryDocumentStore

store = InMemoryDocumentStore()
store.write_documents([Document(content="Haystack wires typed components.")])

pipe = Pipeline()
pipe.add_component("retriever", InMemoryBM25Retriever(document_store=store))
pipe.add_component("prompt", PromptBuilder(template="{{ documents }}\n{{ question }}"))

# explicit sockets: List[Document] -> List[Document]
pipe.connect("retriever.documents", "prompt.documents")

go deeper

for a junior

Be able to write the three-step wiring by heart: create the Pipeline, add_component with a name, connect sender socket to receiver socket. Say plainly that connect() checks names and types right away.

for a middle

Explain where the socket types come from — the run() signature for inputs, the declared output types for outputs — and that a mismatch raises at connect() time rather than at run time.

for a senior

Show why wiring-time validation matters operationally: a mis-wired graph fails in a build-only unit test instead of after a paid embedding and LLM call, and fan-in needs a variadic joiner because plain input sockets hold one connection.

for a principal

Own the tradeoff: explicit typed wiring costs verbosity and buys an inspectable, diffable, testable graph. Argue when that property is worth more than a terser chaining DSL for a team maintaining pipelines over years.

## The model: components with typed sockets A Haystack `Pipeline` is a directed graph. You register a component instance under a name with `add_component("retriever", instance)`, and you draw edges with `connect()`. Each component exposes **input sockets** — derived from the annotated parameters of its `run()` method — and **output sockets** — derived from the output types it declares. A socket is a `(name, type)` pair, and that type is what makes wiring checkable. ## What connect() checks `pipe.connect("retriever.documents", "prompt.documents")` is read as `sender_component.output_socket` → `receiver_component.input_socket`. Haystack verifies, in order: 1. **Both component names are registered.** Connecting to a name you never added is an error, which catches typos in long wiring blocks. 2. **The named sockets exist.** `retriever.docs` when the socket is `documents` fails here. 3. **The types are compatible.** The sender's output type must be acceptable to the receiver's input type. `List[Document]` → `List[Document]` passes; `List[Document]` → `str` does not. A receiver socket typed `Any` accepts anything, and an `Optional[X]` receiver accepts an `X` sender. 4. **The receiver socket is free.** A normal (non-variadic) input socket takes exactly one incoming connection. Pointing a second sender at it is rejected — which is precisely why joiner components with variadic inputs exist. All of this happens when you write the line, not when data flows. That is the single most important property to be able to state in an interview: **Haystack's validation is at wiring time**, so a mis-wired RAG graph blows up in the constructor of your service — or in a unit test that only builds the pipeline — rather than after an embedding call and two LLM tokens. ## The shorthand, and when to avoid it `connect("retriever", "prompt")` omits socket names. Haystack then looks for exactly one type-compatible output/input pair between the two components. If there is exactly one, it wires it. If there are none, or more than one, it raises rather than guessing. The shorthand reads nicely in demos, but explicit socket names are better in code you maintain: adding a second output to a component later can turn a previously unambiguous shorthand into an error, or — worse — silently change which pair is chosen if the component's surface evolves. Explicit names also make the wiring block a readable spec of the dataflow. ## Fan-out is allowed, fan-in is not (without a joiner) One output socket may feed many receivers: connect the same retriever output into a ranker and into a logging component and both get the value. The asymmetry is on the input side. Because a plain input socket holds one connection, two branches that both want to feed the same downstream input must first pass through a component whose input socket is variadic. Knowing this asymmetry explains most "why won't it let me connect this" questions from people new to the framework. ## What is *not* checked at wiring time Type compatibility is structural, not semantic. Two `List[Document]` sockets connect happily even if one carries retrieved chunks and the other expects reranked ones. Sockets typed `Any` — some general-purpose components use them — punt the check to runtime entirely. And a *mandatory* input that is neither connected nor supplied to `run()` is not a wiring error at all: the component simply never becomes runnable, and the pipeline reports that it could not execute. So `connect()` protects you from shape errors, not from wiring the wrong thing to the right type. ## Cycles `connect()` does not forbid an edge that closes a loop; cyclic pipelines are a supported pattern (self-correcting generation, retry-with-feedback). The graph is not required to be acyclic, so "connect() rejects cycles" is a wrong answer. ## Why interviewers ask this It separates people who have shipped a Haystack pipeline from people who have read a quickstart. The follow-up is usually about the cost side: explicit wiring is more verbose than a chain of piped callables, and the compensation is that the graph is inspectable, diffable, serializable and validated before it ever costs you a token.

  • What happens if you omit the socket names and the two components have two compatible pairs?
    Haystack refuses to guess. The shorthand only resolves when exactly one type-compatible output/input pair exists between the two components; with zero or several it raises and reports the candidate sockets so you can disambiguate. That is why explicit `connect("a.out", "b.in")` is the safer style in code that will outlive the demo — adding an output socket to a component later cannot break an explicit edge.
  • If connect() passed, can the pipeline still fail at run time on that same edge?
    Yes. The check is structural, not semantic: two `List[Document]` sockets connect regardless of whether the documents mean the same thing, and sockets typed `Any` bypass the check entirely. Separately, a mandatory input that is neither connected nor supplied in the `run()` input dict never becomes satisfiable, so the component simply never executes and the pipeline reports it as un-run.
  • Does connect() reject an edge that creates a cycle?
    No. Haystack pipelines may contain cycles, and loop-shaped graphs are a supported pattern for validate-and-retry flows. What protects you there is not the wiring check but the per-component run cap configured on the `Pipeline` object, plus a router with a route that leaves the loop.

saying these in an interview costs you the question

  • Says type errors only appear when the pipeline runs
  • Claims Haystack sockets are untyped and coerce values
  • Believes connect() rejects any cycle in the graph
  • Thinks two senders can share one plain input socket
  • Assumes socket names are always optional in connect()

context