Integrations: from stand-in to production
Integrations are the real backends that take the place of the keyless stand-ins this course ran on, each a drop-in for the same call so the crew code does not change.
Last updated: 28 Sep, 2026 · CrewAI 1.15
Every lesson stayed runnable with no key by using a stand-in: a model you wrote, a word-hash embedder, a local store, a server started as a subprocess, a listener that printed events. Production swaps each one for a real backend. This lesson maps them, so a reader leaves knowing what to run for real, not only the stand-in.
What you used against a real one
Each row is the same argument with a different value. The left is what ran here; the middle is the production version; the right is where it comes from.
| What you used | A real one | The package or call |
|---|---|---|
ShopLLM, a BaseLLM you wrote | A hosted model | LLM(model="gpt-4o-mini"); other providers via crewai[...] extras |
word_hash embedder | A hosted or local embedder | an embedder spec, e.g. {"provider": "openai"} or {"provider": "ollama"} |
Memory on a local folder | A shared vector store | Memory(storage="qdrant-edge") for Qdrant |
MCPServerStdio | A remote MCP server | MCPServerHTTP(url="https://...") |
| The event listener from watching | A tracing backend | CrewAI tracing, or an observability vendor |
The model is a true drop-in
The stand-in subclassed BaseLLM, and CrewAI's LLM subclasses BaseLLM too. Because both are the same kind of object, swapping one for the other is one assignment. This line needs a key, so it does not run here.
from crewai import LLM
clerk.llm = LLM(model="gpt-4o-mini") # needs OPENAI_API_KEYThe store, from a folder to a service
Memory writes to a local LanceDB folder by default. Point storage at a path to keep runs together, or at "qdrant-edge" to use Qdrant. The remember and recall calls are the same either way, which the run below shows: one Memory object writes to the folder, and a second one reopens the same folder and reads it back.
View the code here
import hashlib
import math
def word_hash(texts):
vectors = []
for text in texts:
vector = [0.0] * 64
for word in text.lower().split():
word = word.strip(".,?!")
vector[int(hashlib.md5(word.encode()).hexdigest(), 16) % 64] += 1.0
length = math.sqrt(sum(x * x for x in vector)) or 1.0
vectors.append([x / length for x in vector])
return vectors
import json
import os
import re
from crewai import BaseLLM
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
os.environ["CREWAI_TRACING_ENABLED"] = "false"
os.environ["CREWAI_DISABLE_VERSION_CHECK"] = "true"
class ShopLLM(BaseLLM):
script: list = []
def supports_function_calling(self):
return True
def call(self, messages, tools=None, **kwargs):
if isinstance(messages, str):
messages = [{"role": "user", "content": messages}]
if self.script:
return self.script.pop(0)
return self.decide(messages, tools or [])
def decide(self, messages, tools):
last = messages[-1]
if last["role"] == "tool":
return last["content"]
text = last["content"]
orders = re.findall(r"\b[A-Z]\d+\b", text)
want_refund = "refund" in text.lower()
chosen = None
for t in tools:
fn = t["function"]
label = (fn["name"] + " " + (fn.get("description") or "")).lower()
is_refund = "refund" in label
if want_refund and is_refund:
chosen = fn["name"]
break
if not want_refund and not is_refund and ("look up" in label or "status" in label):
chosen = fn["name"]
break
if orders and chosen:
args = json.dumps({"order_id": orders[0]})
return [{"id": f"call_{orders[0]}", "type": "function",
"function": {"name": chosen, "arguments": args}}]
if "working with:" in text:
context = text.split("working with:")[1].strip().split("\n\n")[0]
return f"Dear customer, {context}"
if orders:
return f"I have no way to look up {orders[0]} yet."
return "Hello. Which order is this about?"
A store that outlives the process
import os
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
from crewai import Memory
from crewai.events import crewai_event_bus
from embed import word_hash
from shop_llm import ShopLLM
one = Memory(llm=ShopLLM(model="shop"), embedder=word_hash, storage="./deskmem")
with crewai_event_bus.scoped_handlers():
one.remember("Asha prefers email, not phone calls.",
scope="/customer/asha", categories=["preference"], importance=0.8)
two = Memory(llm=ShopLLM(model="shop"), embedder=word_hash, storage="./deskmem")
for match in two.recall("How does Asha want to be contacted?", depth="shallow", limit=1):
print(match.record.content)Asha prefers email, not phone calls.
Reading the round trip
- The second Memory read the first one's fact from the same folder, so nothing was held only in the first process.
- The API did not change. Only
storagemoved from the default to a path;rememberandrecallare identical. - The same swap reaches Qdrant with
storage="qdrant-edge", which is how a desk shares memory across machines.
Stand-in against production, by concern
| Concern | The stand-in | Production |
|---|---|---|
| Model | Scripted, no key | Hosted, keyed, billed |
| Embedder | Word matching | Trained on meaning |
| Store | A local folder | A shared service |
| Tools | A subprocess | A deployed server |
| Tracing | Printed events | A tracing dashboard |
When to swap each piece
- Swap the model and embedder first, since they decide answer quality.
- Swap the store when more than one process or machine must share memory.
- Keep the stand-ins in your test suite, where a stable output is worth more than a real one.
Related
- Previous: Real model: swapping in a hosted LLM
- Next: Desk state and a reply guardrail
- See also: Watching a crew work
- Reference: CrewAI docs, Observability
- Change
storageto a new folder name and watch a fresh store start empty. - Remember two facts in the first process and recall them in the second.
- Read the docs' embedder page and write the spec for a provider you have a key for.
Every expert started right here.