All contributions
AI & Machine Learninglanggraphqdrantmem0

Orchestrating three agents for a dental clinic with LangGraph, Qdrant, Mem0, and Temporal

Full case: appointments agent + reminders agent + reviews agent, orchestrated with LangGraph and Postgres checkpointer. Mem0 semantic memory, Temporal long-running flows, healthcare guardrails, Langfuse traces.

Numoru EngineeringPublished on June 28, 202619 min read
Share
Implementation proposalgithub.com/numoru-ia/clinic-agents-langgraph

TL;DR

We deployed three specialized agents for a dental clinic: an appointments agent (takes calls/WhatsApp and books), a reminders agent (sends confirmations and pre-visit messages) and a reviews agent (post-visit, asks for a review). All three orchestrated with LangGraph and a Postgres checkpointer, patient memory in Mem0 over Qdrant, long flows (24h reminder, 7-day follow-up) with Temporal, healthcare guardrails with NeMo Guardrails and complete observability in Langfuse. The system reduces no-shows 38%, captures 87% of after-hours leads and frees ~60 hours/month of reception. All code, MCP tools and Temporal workflows are published.

-38%
No-show rate
90-day pilot measurement
+75 pts
After-hours leads captured
12% → 87%
62 hrs
Reception time freed / mo
170 → 108
$420
Platform cost / mo
3-doctor practice, 850 visits

Why three agents and not one giant one

A monolithic agent can answer everything but loses focus. Talking to a patient who wants to book is different from reminding them about tomorrow's visit or asking for a review 24h after treatment. Three benefits of multi-agent design:

  1. Shorter, focused prompts — each agent knows its job exactly.
  2. Independent evaluation — you can improve one without touching the others.
  3. Privilege separation — the reviews agent can never book; the appointments agent never sends bulk emails.

Overall diagram

  ┌──────────────────────────────────────────────────────────┐
  │                    LangGraph orchestrator                │
  │                                                          │
  │   ┌──────────────┐                                       │
  │   │ Router       │ ◄─── Channel (voice / WhatsApp / web) │
  │   └──────┬───────┘                                       │
  │          │                                               │
  │  ┌───────┼──────────┬───────────────┐                    │
  │  ▼       ▼          ▼               ▼                    │
  │ Booking Reminder  Review         Human fallback         │
  │          ▲              ▲                               │
  │          │              │                               │
  │     ┌────┴─────┐   ┌────┴───────┐                        │
  │     │ Temporal │   │ Temporal   │                        │
  │     │ 24h/1h   │   │ 24h post   │                        │
  │     └──────────┘   └────────────┘                        │
  └──────────────────────────────────────────────────────────┘
         │                 │                  │
         ▼                 ▼                  ▼
      Calendar          WhatsApp           Google Reviews
      (MCP)             (MCP)              (MCP)
         │                 │                  │
         └─────────────────┴──────────────────┘
                           │
                           ▼
             Postgres + Qdrant + Redis + Mem0 + Langfuse

Concrete stack

PieceTechnologyRole
OrchestratorLangGraph (Python)State graph between agents
CheckpointerPostgresConversational state persistence
Semantic memoryMem0 over QdrantPer-patient facts
Working memoryRedisPer-turn context
Long flowsTemporal24h reminders, 7d follow-ups
LLMClaude Sonnet via LiteLLMReasoning
VoicePipecat + Deepgram + CartesiaPhone channel
WhatsAppMCP mcp-whatsappMessaging channel
CalendarMCP mcp-calendarAvailable slots
ReviewsCustom MCP mcp-reviewsGoogle Business Profile
GuardrailsNeMo GuardrailsNo diagnosis, no fabricated prices
ObservabilityLangfuse self-hostTraces, evals, sessions

The LangGraph graph

File clinic_graph.py:

from langgraph.graph import StateGraph, END
from langgraph.checkpoint.postgres import PostgresSaver
from typing import TypedDict, Literal

class ClinicState(TypedDict):
    session_id: str
    patient_id: str | None
    channel: Literal["voice", "whatsapp", "web"]
    user_message: str
    intent: Literal["book", "reschedule", "cancel", "ask_info", "confirm", "review", "unknown"]
    working_memory: dict
    response: str
    tool_calls: list
    done: bool

def router_node(state: ClinicState) -> ClinicState:
    state["intent"] = classify_intent(state["user_message"])
    return state

def booking_node(state: ClinicState) -> ClinicState:
    # specialized agent with its own prompt + calendar tools
    result = booking_agent.run(state)
    return {**state, **result}

def reminder_handler(state: ClinicState) -> ClinicState:
    # handles reminder confirmations
    result = reminder_agent.run(state)
    return {**state, **result}

def review_handler(state: ClinicState) -> ClinicState:
    result = review_agent.run(state)
    return {**state, **result}

def human_fallback(state: ClinicState) -> ClinicState:
    trigger_human_handoff(state)
    return {**state, "done": True, "response": "I'll connect you with a receptionist."}

def route(state: ClinicState) -> str:
    intent = state["intent"]
    if intent in ("book", "reschedule", "cancel", "ask_info"):
        return "booking"
    if intent == "confirm":
        return "reminder"
    if intent == "review":
        return "review"
    return "fallback"

graph = StateGraph(ClinicState)
graph.add_node("router", router_node)
graph.add_node("booking", booking_node)
graph.add_node("reminder", reminder_handler)
graph.add_node("review", review_handler)
graph.add_node("fallback", human_fallback)

graph.set_entry_point("router")
graph.add_conditional_edges("router", route, {
    "booking": "booking",
    "reminder": "reminder",
    "review": "review",
    "fallback": "fallback",
})
graph.add_edge("booking", END)
graph.add_edge("reminder", END)
graph.add_edge("review", END)
graph.add_edge("fallback", END)

checkpointer = PostgresSaver.from_conn_string(os.getenv("POSTGRES_URL"))
app = graph.compile(checkpointer=checkpointer)

Booking agent

Short prompt, specific tools, guardrails:

BOOKING_SYSTEM = """
You are the booking agent for Numoru Dental Clinic.
Your single task is helping book, reschedule or cancel appointments.

Rules:
- Confirm full name and phone before booking.
- Honor hours: Mon-Sat 9:00-19:00.
- If the patient asks for price or diagnosis, use search_clinic_info;
  if it's not in the RAG, offer the doctor — never make it up.
- If you detect a real medical urgency, transfer immediately with transfer_to_emergency.
"""

booking_agent = Agent(
    system_prompt=BOOKING_SYSTEM,
    model="claude-sonnet",
    tools=[
        tool.find_available_slot,
        tool.book_appointment,
        tool.reschedule,
        tool.cancel,
        tool.search_clinic_info,
        tool.transfer_to_emergency,
    ],
    guardrails=NeMoGuardrails(config="guardrails/clinic.colang"),
)

Reminders agent (with Temporal)

The reminders agent is reactive (replies) but Temporal triggers the send:

# Temporal workflow
@workflow.defn
class AppointmentReminderWorkflow:
    @workflow.run
    async def run(self, appt: Appointment):
        # 24h before
        await workflow.sleep(appt.start - timedelta(hours=24))
        await workflow.execute_activity(
            send_whatsapp_template,
            ReminderInput(patient_id=appt.patient_id, slot=appt.start, kind="24h"),
            start_to_close_timeout=timedelta(minutes=2),
        )
        # 1h before
        await workflow.sleep(appt.start - timedelta(hours=1))
        await workflow.execute_activity(
            send_whatsapp_template,
            ReminderInput(patient_id=appt.patient_id, slot=appt.start, kind="1h"),
            start_to_close_timeout=timedelta(minutes=2),
        )

Temporal survives restarts, upgrades and worker crashes. If the patient replies via WhatsApp, the inbound message triggers the LangGraph graph with intent="confirm".

Reviews agent (post-visit)

@workflow.defn
class PostVisitReviewWorkflow:
    @workflow.run
    async def run(self, appt: Appointment):
        # 4h post-visit
        await workflow.sleep(appt.end + timedelta(hours=4))
        satisfied = await workflow.execute_activity(
            nps_one_question,
            NpsInput(patient_id=appt.patient_id),
            start_to_close_timeout=timedelta(minutes=10),
        )
        if satisfied >= 8:
            # 24h later, ask for a public review
            await workflow.sleep(timedelta(hours=20))
            await workflow.execute_activity(
                request_review,
                ReviewInput(patient_id=appt.patient_id, channel="google"),
            )

The agent doesn't ask unsatisfied patients for a review — instead it triggers an internal workflow so a human reaches out first.

Patient memory with Mem0

Facts extracted from past conversations (with consent):

from mem0 import Memory

memory = Memory.from_config({
    "vector_store": {
        "provider": "qdrant",
        "config": {"host": "qdrant", "port": 6333, "collection_name": "clinic_patients"},
    },
    "llm": {"provider": "litellm", "config": {"model": "claude-sonnet"}},
})

def enrich_prompt_with_memory(patient_id: str, query: str) -> str:
    facts = memory.search(query=query, user_id=patient_id, limit=5)
    if not facts:
        return ""
    bullets = "\n".join(f"- {f['memory']}" for f in facts)
    return f"\n\nPatient prior context:\n{bullets}\n"

Useful fact examples:

  • "Prefers afternoon appointments"
  • "Allergic to penicillin"
  • "Last 2 visits: cleaning and root canal on molar 36"
  • "Has GNP insurance"

Healthcare guardrails

File guardrails/clinic.colang:

define user ask medical_diagnosis
  "do I have a cavity?"
  "what disease do I have?"
  "is it serious?"
  "do I need braces?"

define bot refuse medical_diagnosis
  "We need to see you to diagnose. Shall I book you with the doctor?"

define user ask price
  "how much does it cost?"
  "what's the price?"

define flow
  user ask medical_diagnosis
  bot refuse medical_diagnosis

define flow
  user ask price
  execute search_clinic_info
  # if no price found in RAG, the tool returns an error and the default flow says "ask reception"

Observability in Langfuse

Each graph execution emits a session with:

  • patient_id (hash).
  • channel.
  • detected intent.
  • Tool calls with input/output.
  • "Goal completion" score (automatic eval: did it close the user's intent?).

Dashboards we look at:

  • Goal completion rate per intent (target >85% for book).
  • Human transfer rate (target <12%).
  • p95 latency (voice: <2.5s per turn).
  • No-show rate post-reminder (baseline vs actual).

Real metrics from the pilot client

Dental clinic with 3 doctors, 850 visits/month.

MetricPre90 days post
No-show rate22%14%
After-hours leads captured12%87%
Reception hours/month170108
Average NPS7.88.4
New Google reviews/month418
Operational agent cost (USD/month)420

ROI: the additional receptionist they didn't hire costs ~$600 USD / month. The agent costs less and works 24/7.

Pilot client — 90-day deltas (indexed, pre = 100)

Same practice, 3 doctors, 850 visits per month. Agents deployed in sequence (booking → reminders → reviews) over 6 weeks; stabilization period observed at day 60.

After-hours captureGoogle reviews / moReception hoursNo-shows0200400600800
  • Pre (baseline = 100)
  • 90 days post

Numoru client telemetry, measured against previous-year baseline.

Business & commercial impact

Business & commercial impact

Where the value concentrates

Multi-agent setups unlock revenue the voice agent alone cannot. Reminders is the single biggest dollar line — every point of no-show recovered is a $120-180 slot that gets backfilled. Reviews is the long-tail growth engine — organic new patients arrive through Google Maps rank, which moves with fresh 5-star reviews. The booking agent is table stakes; the other two are the moat.

Where this pattern sells beyond dental

Multi-agent orchestration ticket by vertical (Numoru, USD)

Dental groups (3-10 sites)
Booking + reminders + reviews + insurance eligibility.
$1,200 – 3,500 / mo
12 mo
Aesthetic / MedSpa chains
Deposit collection + pre-care prep + 7-day follow-up + NPS.
$1,800 – 4,500 / mo
12 mo
Physiotherapy / rehab
Plan adherence + session reminders + insurance renewals.
$900 – 2,400 / mo
12 mo
Veterinary chains
Vaccination reminders + prescription refill + annual wellness.
$1,100 – 2,800 / mo
12 mo
Private schools / tutoring
Enrollment agent + parent comms + event reminders + NPS.
$1,400 – 3,200 / mo
9-month school year
Boutique fitness
Trial booking + attendance nudges + win-back + reviews.
$700 – 1,800 / mo
12 mo

Public benchmarks anchoring the value

Public case studyDental practice management · USA · 2024

Dental Intelligence — schedule & reminder analytics

Challenge
Quantify the lift from automated reminders + re-care recall on a practice's books.
Solution
Aggregated telemetry from 12,000+ practices using their Recall Manager and reminder suite.
Results
No-show reduction
-31%
With SMS + voice reminders
Re-care reactivation
18%
Of lapsed patients respond to automated recall
Incremental production
$4,200 / mo
Avg 8-chair practice
Public case studySearch · Global · 2023

Google — local search + reviews impact

Challenge
Measure how review volume and recency shift Google Maps rank and click-through for local businesses.
Solution
Google Business Profile studies combined with independent MOZ Local Search Ranking Factors report.
Results
Top-3 Maps rank factors
Reviews
Among top 5 weighted signals
CTR at 4.8+ stars vs <4.0
+93%
On local pack results
Review-driven organic visits
+23%
Year-over-year, small biz
Public case studyAI framework · Global · 2024-2025

LangChain / LangGraph — multi-agent production case studies

Challenge
Document production deployments using LangGraph checkpointers for long-running agentic work.
Solution
LangChain's customer story library covers deployments at Replit, LinkedIn, Rakuten and others orchestrating 2-10 specialized agents.
Results
Reported dev velocity
2-3× faster
Vs monolithic agent
Eval granularity
Per-agent
Easier to isolate regressions
State durability
Postgres
Survives restart, supports replay

Illustrative case — aesthetic chain

Illustrative caseAesthetic healthcare · 6 clinics · 48 staff · $4.8M ARR · Mexico + USA (Miami)

Aesthetic / MedSpa chain (6 locations) adopting the 3-agent stack

Baseline
No-show rate 26% (electives don't feel urgent). $0 deposit collection. 11 Google reviews / mo across 6 locations. 2 receptionists per location, $3.1k / mo blended each.
Intervention
Multi-agent rollout: booking + deposit collection agent, reminder agent with pre-care instructions, reviews agent with photo release + testimonial flow. Temporal workflows for 7 / 14 / 30-day post-treatment follow-up.
Projected outcome (12 mo)
No-show rate
26% → 13%
Deposit + reminders combined
Deposit collection rate
0% → 72%
Default when booking
Reviews / mo (all sites)
11 → 94
Sustained over 6 mo
New patients from search
+41%
Google Maps rank lift
Annual revenue lift
+$980K
Incremental bookings + fewer gaps
Platform cost
$28,800 / yr
6 × $400 × 12 tier
Revenue model grounded in MedSpa Boss 2024 operational benchmarks and Google Business Profile CTR data. Synthetic case — not a specific Numoru client.

ROI calculator — 3-doctor dental practice

Single dental practice, 3 doctors, 850 visits / mo (12 months)

Payback: < 1
Assumptions
Visits / month (baseline)850
Average procedure value$180
No-show rate pre-agent22%
No-show rate post-agent14%
After-hours capture pre12%
After-hours capture post87%
After-hours inbound leads / mo90
Lead → patient conversion28%
Platform retainer (12 mo × $420)−$5,040
Implementation (one-time)−$6,500
No-show recovery (+68 visits / mo × $180 × 12)+$146,880
New patients from after-hours capture (+19 / mo × $720 LTV × 12)+$164,160
Reception hours saved (62 × $12 × 12)+$8,928
Review-driven new visits (+6 / mo × $720 × 12)+$51,840
Net year-1 gross contribution+$360,268

Pricing tiers Numoru sells

Single-practice
$420/ month
One clinic. All three agents.
  • 1 location / 1 phone / 1 WhatsApp
  • LangGraph orchestration + Temporal
  • Mem0 patient memory
  • NeMo healthcare guardrails
  • Langfuse monthly dashboard
  • Google Calendar + Google Reviews
  • Setup: $6,500 one-time
Multi-site group
$320/ clinic / mo
3-10 locations under one KB.
  • Shared Qdrant knowledge base
  • Per-clinic branding + voice
  • Cross-site analytics dashboard
  • Insurance eligibility add-on available
  • Dentrix / OpenDental integration
  • Quarterly eval review
  • Setup: $14,000 one-time
Franchise / enterprise
$240/ clinic / mo
10+ sites, white-label available.
  • Self-hosted option (HIPAA scope)
  • White-label agent branding
  • Master contract / PO friendly
  • Dedicated CSM
  • Custom guardrails + policies
  • Annual compliance audit
  • Setup: $28,000+ one-time

Monthly includes a usage bucket (2,000 calls + 5,000 WhatsApp + 8,000 LLM inferences per clinic). Overages bill at rate card.

Deploy on the OSS stack

On top of the self-hosted droplet stack, we add:

services:
  clinic-orchestrator:
    image: numoru/clinic-orchestrator:latest
    environment:
      POSTGRES_URL: postgres://lf:${LF_DB_PASSWORD}@langfuse-db:5432/clinic
      QDRANT_URL: http://qdrant:6333
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/3
      LITELLM_URL: http://litellm:4000
      LANGFUSE_PUBLIC_KEY: ${LF_PUBLIC_KEY}
      TEMPORAL_HOST: temporal:7233
    depends_on: [langfuse-db, qdrant, redis, litellm, temporal]

  temporal:
    image: temporalio/auto-setup:1.25
    environment:
      DB: postgres12
      DB_PORT: 5432
      POSTGRES_USER: lf
      POSTGRES_PWD: ${LF_DB_PASSWORD}
      POSTGRES_SEEDS: langfuse-db
    networks: [core]

  clinic-temporal-worker:
    image: numoru/clinic-temporal-worker:latest
    environment:
      TEMPORAL_HOST: temporal:7233
      LITELLM_URL: http://litellm:4000
    depends_on: [temporal]

Compliance and regulation

  • LFPDPPP (Mexico): explicit consent for memory; /patient/{id}/data endpoint for access/deletion.
  • HIPAA (if US patient): all on-prem, no data leaves the client's droplet.
  • AI Act (if EU patient): transparency in the voice channel ("this assistant is AI"), auditable Langfuse logs, clear human supervision plan (clear fallback).

Tests and evals

  • Unit tests for each MCP tool.
  • Integration tests for the graph with embedded Postgres.
  • Eval suite with 80 golden scripts per agent.
  • Manual weekly E2E: reception team reviews 10 random conversations.

Common anti-patterns

  1. One agent that does everything. 5k-token prompts, impossible evals, risky changes.
  2. Reminders via cron. No durability, server restart loses them. Temporal is the answer.
  3. Global memory without user_id. Data leak between patients. Mandatory user_id on every Mem0 operation.
  4. No human in the loop. Guardrails fail sometimes; need a clear transfer path and edge-case alerts.
  5. Deploy without backup. An accidental drop table appointments is fatal. Daily Restic at minimum.

FAQ

Can it adapt to other verticals (law firm, vet, spa)?Yes. Same skeleton; tools (services catalog), guardrails and tone change. Generally 1-2 weeks of adaptation per new vertical.

How many agents does the $40 droplet handle?Comfortably 3-5 small clients on the same droplet. Beyond 10, separate Temporal to its own instance.

Does it work without Temporal?You can use a simple cron + Postgres as queue. It works until a restart fails and you understand why everyone uses Temporal/Inngest.

How do you handle patients who don't accept AI?At the start of the voice/WhatsApp channel they are informed and can say "I want to talk to a human" — the graph immediately transfers.

Is it more expensive than hiring a receptionist? By month 2 in mid-volume cases. With very small clinics (<200 visits/month) a part-time receptionist is still cheaper.

Next steps

Repo: github.com/numoru-ia/clinic-agents-langgraph. Includes prompt seeds, synthetic test data, vertical adaptation playbooks and operations guides (incident runbook, escalation, compliance). The next article in the series covers CI/CD evals on this kind of agent.

Want results like these for your company?

Start a conversation
Share