In LangChain, how do bind, with_config and configurable_fields differ on a Runnable?
answer
- Three planes: component args, framework config, deferred knobs
- Every method returns a new runnable
- Bind is fixed, configurable is caller-chosen
- Tags and callbacks propagate down the tree
- Scope retry to the flaky step only
basics
~10 sbind fixes call-time arguments passed into the runnable itself. with_config attaches framework-level config such as tags, callbacks and run_name. configurable_fields leaves a parameter open so the caller can override it per invocation through config['configurable'].
solid answer
~40 sAll three return a **new** runnable and never mutate the original, which matters because chains are usually module-level values shared across requests. `.bind(**kwargs)` pins arguments that are forwarded into the underlying component's own call — `model.bind(stop=["\n\n"])` sends that stop sequence on every invocation. `.with_config(...)` attaches a `RunnableConfig`: `tags`, `metadata`, `run_name`, `callbacks`, `max_concurrency`, `recursion_limit` — framework concerns that propagate to nested steps and show up in traces, not model parameters. `.configurable_fields(...)` is the deferred version of `bind`: you declare a parameter as a `ConfigurableField` with an id, and callers override it at invoke time via `config={"configurable": {"that_id": value}}`. `.configurable_alternatives(...)` does the same for swapping a whole sub-runnable. The rule of thumb: bind for what is fixed by the chain's design, configurable_fields for what a caller legitimately varies per request, with_config for observability and execution limits.
code
python · 10 linesfrom langchain_core.runnables import ConfigurableField, RunnableLambda
base = RunnableLambda(lambda x: x)
tagged = base.with_config(run_name="echo", tags=["demo"]) # framework-level
resilient = base.with_retry(stop_after_attempt=3) # wraps, returns new
safe = base.with_fallbacks([RunnableLambda(lambda x: "default")])
print(tagged.invoke("hi"), resilient.invoke("hi"), safe.invoke("hi"))
print(ConfigurableField(id="llm_temperature").id)go deeper
Know that bind pins an argument onto a model or tool call and returns a new runnable, and that you must assign the result rather than calling bind for its side effect.
Distinguish the planes: bind feeds the component's own call, with_config carries framework concerns like tags, callbacks and run_name, and configurable_fields defers a value to the caller via config['configurable'].
Show operational judgment — scope with_retry to the flaky step so you do not re-pay for upstream work, use with_fallbacks to degrade to another provider, and keep non-idempotent steps out of any retried scope.
Treat configurable ids as public API. Decide which knobs callers may turn, keep the surface small and named centrally, and be explicit that a mistyped id fails silently, so every override needs a test that proves it changed behaviour.
## Three different layers It helps to see these as operating on three distinct planes. **Component arguments** — things the wrapped component understands, like a stop sequence or a tool schema. `bind` lives here. It produces a `RunnableBinding` that merges the bound kwargs into every call of the wrapped runnable. Because it is a wrapper, `model.bind(stop=["\n"])` leaves `model` untouched; forgetting the assignment (`model.bind(...)` on its own line) is a classic no-op bug. **Framework config** — `RunnableConfig`, which the framework itself interprets: `callbacks` for tracing and metrics, `tags` and `metadata` for filtering runs in an observability backend, `run_name` for labelling a step, `max_concurrency` for the width of batch and fan-out, `recursion_limit` as a guard, and `configurable` as the override channel described below. `.with_config()` attaches these statically to a runnable, and a config passed at `invoke`/`stream`/`batch` time merges with them. Config propagates *down* into nested steps, which is why a tag set at the top of a chain lets you find every child call of that request in a trace. **Deferred parameters** — `.configurable_fields()` marks a constructor parameter as overridable and gives it a stable id. The caller then does `chain.invoke(x, config={"configurable": {"llm_temperature": 0.9}})`. `.configurable_alternatives()` extends the idea to whole components: declare a `ConfigurableField` id plus named alternatives and a `default_key`, and the caller selects the variant by name. Both are the mechanism behind "one chain object, per-tenant behaviour". ## Choosing between bind and configurable_fields The question to ask is *who owns the value*. If the chain's correctness depends on it — a stop sequence that keeps the model from rambling past the answer, a fixed response format — bind it, so no caller can break the chain by overriding. If the value is a product knob that varies per request or per tenant — temperature, model choice, top-k — make it a configurable field, and document the id, because the id is now part of your API surface. Making everything configurable is a real cost: every id is a contract, and a typo'd id silently does nothing rather than raising, which is one of the sharper debugging traps in this area. ## The resilience decorators Two more methods belong in the same family because they follow the same wrap-and-return-new pattern: - `.with_retry(stop_after_attempt=..., retry_if_exception_type=..., wait_exponential_jitter=True)` wraps the runnable in retry logic. Scope it to the step that is actually flaky, not the whole chain — retrying an entire chain re-runs everything upstream, doubling cost and re-emitting side effects. - `.with_fallbacks([alt1, alt2], exceptions_to_handle=(...))` invokes the alternatives in order when the primary raises. This is how you degrade to a cheaper or different provider. The alternatives receive the *same input*, so they must accept the same shape. Because a composed chain is itself a Runnable, all of these apply at any granularity — one step or the whole pipeline. Choosing the granularity deliberately is most of the skill. ## Idempotency and side effects Retry and fallback both re-invoke. If a step inside the retried scope writes to a database or posts a message, retrying duplicates that effect. Keep side-effecting steps outside retry scopes, or make them idempotent with a request key. ## Common mistakes - **Mutation assumptions.** These methods return new objects. Assigning is mandatory. - **Putting model parameters in `with_config`.** A temperature passed as config metadata is simply ignored by the model; nothing errors. - **Wrapping the whole chain in retry** when only the model call is flaky, so a transient 429 re-runs retrieval and re-pays for it. - **Silent configurable ids.** An unknown key under `configurable` is not an error; the run just uses the default, and you chase a phantom. - **Bound arguments compounding.** Since `bind` returns a wrapper, derive each variant from the base runnable rather than from another bound variant, or the bindings stack up in ways you did not intend.
- Why is wrapping an entire chain in with_retry usually the wrong granularity?Because retry re-invokes everything in its scope. A transient rate-limit error on the model call would re-run every upstream step — retrieval, parsing, any external fetch — paying their latency and cost again, and re-triggering any side effects they perform. Wrap the specific flaky component instead, and keep non-idempotent steps outside any retry scope.
- A caller passes config={'configurable': {'temprature': 0.9}} with a typo. What happens?Nothing visible: an unrecognised key under `configurable` matches no declared ConfigurableField id, so the runnable uses its default and the run succeeds with the wrong setting. That silence is why configurable ids should be few, named centrally as constants, and covered by a test that asserts the override actually changes behaviour.
- How do with_fallbacks and configurable_alternatives differ, given both can swap in another model?`with_fallbacks` is reactive: the alternative runs only when the primary raises a handled exception, so it is a resilience mechanism. `configurable_alternatives` is proactive: the caller names which variant to run before the call, so it is a routing or A/B mechanism. They compose — a per-tenant alternative can itself carry fallbacks.
saying these in an interview costs you the question
- Thinking bind mutates the model in place
- Passing temperature through with_config and expecting effect
- Retrying the whole chain when one call is flaky
- Assuming an unknown configurable id raises an error
- Believing fallbacks retry the same runnable