Skip to content
derpx06Notes on systems, models & learning
2. LCEL & Prompt Architecture · lesson 12 of 68 · 4 min · January 10, 2026

RunnableLambda & Custom Logic

Why you should move deterministic logic out of the LLM and into code.

The limitations of purely probabilistic architectures are becoming starkly apparent. Reliance on LLMs for control flow and arithmetic results in fragile, expensive systems. RunnableLambda is the architectural bridge that injects rigid, deterministic code into fluid, generative pipelines.

We will explore this through 8 Core Architectural Pillars.


The most immediate argument is economic. Delegating logic to an LLM (e.g., "format this date") is renting a supercomputer to do a pocket calculator's job.

Every instruction you add to a prompt ("Don't use markdown", "Output JSON") consumes context window and dilutes attention. By moving this to code, you eliminate the "Token Tax" and the "Lost in the Middle" phenomenon.

FeatureLLM-Based Logic (Prompting)Code-Based Logic (RunnableLambda)
CostHigh (Per-token)Negligible (CPU cycles)
ReliabilityProbabilistic (<100%)Deterministic (100%)
LatencyHigh (Network I/O)Low (Microseconds)

Data entering an AI pipeline is rarely pristine. RunnableLambda acts as the Sentinel, sanitizing inputs before they reach the prompt.

User input often contains invisible characters, HTML artifacts, or junk data that confuses tokenizers.

sanitizer.py
import html
import unicodedata
from langchain_core.runnables import RunnableLambda

def sanitize_text(text: str) -> str:
  # 1. Remove HTML artifacts (e.g., &amp;)
  text = html.unescape(text)
  # 2. Normalize unicode to avoid 'invisible' chars or confusing glyphs
  text = unicodedata.normalize("NFKC", text)
  # 3. Hard limit on length to prevent context overflow attacks
  return text[:10000].strip()

# The Sentinel Layer
sanitization_layer = RunnableLambda(sanitize_text)

Don't ask the LLM to "treat 'mbp' as 'MacBook Pro'". Just map it.

canonicalizer.py
def canonicalize_query(query: str) -> str:
  mappings = {
      "mbp": "MacBook Pro",
      "ai": "Artificial Intelligence",
      "js": "JavaScript"
  }
  # Deterministic dictionary lookup is O(1) and 100% accurate.
  return mappings.get(query.lower(), query)

resolver_layer = RunnableLambda(canonicalize_query)

Raw LLM output is text. But your database needs Integers, Booleans, and ISO Dates. RunnableLambda is the Casting Layer.

Does "five" equal 5? To Python's int(), no. To a robust lambda, yes.

casting.py
def robust_int_cast(text: str) -> int:
  # Handle verbal numbers if needed, or strip 'stars' suffix
  text = text.lower().replace("stars", "").strip()
  try:
      return int(text)
  except ValueError:
      # Fallback logic defined in CODE, not hallucinated.
      # Maybe return a default, or raise a specific error for retry.
      return 0

casting_layer = RunnableLambda(robust_int_cast)

LLMs love to chat ("Here is the JSON: ..."). A lambda can surgically extract the payload.

json_cleaner.py
import json
import re

def extract_json(text: str) -> dict:
  # Regex to find the first { and the last }
  match = re.search(r"{.*}", text, re.DOTALL)
  if not match:
      raise ValueError("No JSON found")
  return json.loads(match.group())

json_cleaner = RunnableLambda(extract_json)

Efficiency means knowing when not to run a model. If a request is invalid, stop immediately.

If a specialized legal bot receives a medical query, detect it with code and exit.

circuit_breaker.py
class OutOfScopeError(Exception):
  pass

def circuit_breaker(query: str):
  forbidden_terms = ["medical", "doctor", "prescription"]
  if any(term in query.lower() for term in forbidden_terms):
      # Raise an error to stop execution immediately
      raise OutOfScopeError("This bot handles legal queries only.")
  return query

# Usage: Chain stops here if invalid. No GPU tokens wasted.
guard_layer = RunnableLambda(circuit_breaker)

For simple logic, you don't need a RunnableBranch. A Python ternary operator is cleaner.

micro_branch.py
# Pass "summary" if text is long, else pass raw "text"
# This "Micro-Branching" handles edge cases linearly.
smart_router = RunnableLambda(
  lambda x: x['summary'] if len(x['text']) > 1000 else x['text']
)

Components often speak different dialects. RunnableLambda acts as the Adapter Pattern, reshaping data structures on the fly.

Decouple your prompt from your loader. If the loader outputs pdf_content but the prompt expects context.

adapter.py
# Adapter: Maps specific PDF output to generic prompt inputs
adapter = RunnableLambda(lambda x: {
  "context": x["pdf_content"],
  "question": x["user_message_v2"]
})

# seamless composition
chain = pdf_loader | adapter | summarization_chain

Models return AIMessage objects. Tools expect strings. Provide the glue.

unwrapper.py
# Strip the object wrapper, extract the string payload
unwrap = RunnableLambda(lambda msg: msg.content)

# Now the output is compatible with simple string tools
chain = model | unwrap | simple_tool

LLMs are structurally incapable of reliable arithmetic (they predict tokens, they don't calculate). Never ask an LLM to add.

LLM -> Code -> LLM.

  1. Extract inputs (LLM).
  2. Calculate result (Code).
  3. Synthesize answer (LLM).
math_layer.py
def reliable_math(inputs):
  # Deterministic math. Cannot hallucinate.
  salary = inputs["salary"]
  rate = inputs["tax_rate"]
  return salary * (1 - rate)

math_layer = RunnableLambda(reliable_math)

An LLM cannot reliably "sort this list by price". Python's tim_sort is O(N log N) and mathematically proven correct.

sorting.py
def deterministic_sort(items):
  # Sort by price ascending, then rating descending
  return sorted(items, key=lambda x: (x['price'], -x['rating']))

sorter = RunnableLambda(deterministic_sort)

Don't embed business rules in prompts ("If user is in CA..."). Rules change; prompts shouldn't have to.

Execute the rule in code. Inject the result into the prompt.

logic_injection.py
def calculate_shipping(user_profile):
  # Complex proprietary logic
  if user_profile['state'] == 'CA' and user_profile['spend'] > 100:
      return 0
  return 15.99

# The prompt receives: "Shipping Cost: $0"
# It doesn't need to know WHY. It just reports it.
shipping_calculator = RunnableLambda(calculate_shipping)

Inject real-time state that the model cannot know (Time, User Location).

time_injection.py
from datetime import datetime

# The model now knows exactly what "Today" means
time_injector = RunnableLambda(lambda _: datetime.now().isoformat())

chain = (
  RunnablePassthrough.assign(current_time=time_injector)
  | prompt # Prompt can now use {current_time} inside the template
  | model
)

Code needs to be robust, testable, and observable.

Blocking IO kills web servers. If your lambda calls an API, make it async.

async_fetch.py
import aiohttp

async def fetch_weather(city):
  async with aiohttp.ClientSession() as session:
      async with session.get(f"https://api.weather.com/{city}") as resp:
          return await resp.json()

# LangChain automatically handles the async execution
weather_fetcher = RunnableLambda(fetch_weather)

Debug chains without breaking them.

probe.py
# Prints state to console and returns it unchanged
debug_probe = RunnableLambda(lambda x: print(f"DEBUG: {x}") or x)

chain = step1 | debug_probe | step2

The evolution of AI engineering is moving from "Prompt Hacking" to "Flow Engineering." RunnableLambda is your primary tool for this. It allows you to:

  1. Stop paying for simple logic (Token Tax).
  2. Guarantee correctness (Determinism).
  3. Test your business rules (Unit Testing).

Treat the LLM as a Reasoning Engine, not a Computing Engine. Use Python for the rest.