tars · chat · greeting
TARS chat widget — floating bubble greeting a visitor on markandrewmarquez.com
// cover · TARS greeting a visitor from the floating chat bubble on this site

Sales teams spend an enormous amount of time doing the same five things on every inbound lead: asking about pain points, qualifying budget and timeline, capturing contact info, logging the conversation, and scheduling follow-up. It's repetitive, it doesn't scale, and it's the sort of work an LLM was arguably built for — except most "AI chatbots" still feel like branching decision trees with a language model bolted on top.

I wanted to build something different: a chatbot that actually feels like a conversation, that can hold state across turns, that plugs into a real CRM, and that hands finished leads to an AI agent for follow-up rather than dumping them in a queue. The result is TARS — live right now in the chat bubble at the bottom-right of this page.

01 // The Challenge

Two problems were really one problem. First: manual lead qualification doesn't scale. A small consulting practice can't have a human answering every "hey, do you do X?" message at 11pm on a Tuesday, and most of those conversations end up being unqualified anyway. Second: the chatbots that try to solve this feel robotic. They ask questions in a rigid order, can't handle objections, repeat themselves, and forget context the moment the visitor says something unexpected.

The brief I set for myself: build a sales bot that holds a natural, multi-turn conversation, qualifies the lead against real criteria (budget, timeline, company size, decision-maker status, pain points), captures contact info without feeling pushy, and hands off to Salesforce the moment it's done — with an AI follow-up agent taking over from there. No humans in the loop from first hello to follow-up email draft.

02 // The Stack

Every piece of this stack was picked to solve a specific problem, not because it was trendy.

03 // Architecture Overview

Visitor's Browser  (S3 + CloudFront · markandrewmarquez.com)
      ↓  2-line embed snippet
Azure Static Web Apps  (widget.js + chat-widget.css + tars-avatar.svg)
      ↓  POST /chat/stream  (SSE)
Azure Container Apps  (FastAPI + LangGraph · checkpoints in Neon Postgres)
      ↓  ainvoke()
LLM Provider  (xAI Grok in prod · Claude / GPT-4o / Llama swappable)
      ↓  simple_salesforce
Salesforce  (Lead + Task records, custom qualification fields)
      ↓  Record-Triggered Flow (After Insert)
Agentforce Agent  (Lead Qualification Follow-Up)
      ↓
Follow-up Tasks · Email Draft · Opportunity (if score ≥ 80)

The widget code is served from a completely different origin than the website that embeds it — the public portfolio repo, served as a static site from S3 behind CloudFront, carries nothing but a 2-line <script> snippet. Widget code, chat UI, and avatar all live on Azure Static Web Apps, and the backend runs as a Docker container on Azure Container Apps. All three layers can be redeployed independently.

tars · conversation · qualification
TARS mid-conversation — asking about budget and timeline during qualification
// TARS mid-conversation — exploring pain points and qualifying on budget + timeline

04 // The 7-Stage Conversation Graph

The thing that makes this feel like a conversation instead of a form is the graph structure. Each turn the visitor takes, the state flows through an extraction node (pulls any new contact / qualification data out of the message), then a router node (decides what to do next), then lands at the node that fits the current stage.

▸ 01
GreetingWarm opening question. Introduces TARS, invites the visitor to share what brought them by.
▸ 02
DiscoveryExplores pain points, current tools, and goals — the "why are you here" phase.
▸ 03
QualificationBudget range, timeline, company size, decision-maker status — one question at a time, with budget and decision-maker as the priority.
▸ 04
Objection HandlingAddresses concerns with empathy and value reinforcement, then returns to the main flow.
▸ 05
Lead CaptureName, email, company. Asked naturally at the point the conversation has earned it — ideally once budget and decision-maker status are known.
▸ 06
ConfirmationSummarises captured details and ends with "Does that all look correct?"

The seventh stage — Complete — is where things stop being conversational. Once the visitor confirms, the graph fires the scoring node (0–100 score with a breakdown), then the Salesforce node (Lead + Task creation), and the conversation is done.

What stops it feeling scripted is the router. Instead of a hardcoded stage order, the router is itself an LLM call that looks at the current state, the latest message, and what's already known, and decides where to go next. If a visitor volunteers their budget during discovery, the extraction node captures it and qualification only asks about what's still missing. The router is told to get budget and decision-maker status before it asks for contact details — pain points and company size, which visitors tend to offer during discovery, don't count. If the visitor dodges those questions or tries to leave, it moves on with what it has rather than looping. If they suddenly raise an objection mid-qualification, the router pivots to objection handling and then comes back. The flow adapts to the human, not the other way around.

05 // What Makes It Different

A basic chatbot wraps a system prompt around an LLM and calls it a day. A few features separate this from that:

06 // Hardening & Evals

A bot on a public page gets poked at. Since the first version shipped, most of the work has gone into making TARS hard to abuse, easy to observe, and measurable rather than judged by feel.

07 // Closing the Loop with Agentforce

This is the part most chatbot projects skip: what happens after the lead is created. A record in Salesforce that no one actions is just data entry. TARS hands off to an Agentforce agent that does the actual follow-up work.

salesforce · lead · custom fields
Salesforce Lead record populated with custom qualification fields from the TARS conversation
// Salesforce Lead record — populated with the 6 custom qualification fields

The flow is simple and all-automated. When TARS creates a Lead, six custom fields get populated: Lead Score, Budget Range, Timeline, Company Size, Pain Points, and Lead Source. The full chat transcript is attached as a linked Task record so the sales team has complete context without reading a JSON blob. A Record-Triggered Flow fires on insert, invokes the Agentforce agent, and the agent takes over.

salesforce · task · transcript
Salesforce Task record containing the full chat transcript from the conversation
// Salesforce Task — full chat transcript linked to the Lead

The Agentforce agent reads the Lead + transcript and executes four actions in a single turn, all wired to a consolidated Apex class: updates the Lead rating, creates prioritised follow-up Tasks with due dates, drafts a personalised follow-up email that references specific pain points from the conversation, and conditionally creates an Opportunity for high-value leads (score ≥ 80 and budget ≥ $50K). Zero human intervention between the visitor saying "yes, that looks correct" and a ready-to-send email draft landing in the sales team's queue.

agentforce · follow-up · actions
Agentforce agent output — tasks, email draft, and opportunity created automatically
// Agentforce output — tasks, email draft, and opportunity created in one agent turn

Agent Script CLI Workflow

The Agentforce agent itself is authored as a .agent script file in version control, not assembled click-by-click in the Salesforce UI. The Salesforce CLI's Agent Script plugin (sf agent validate, sf agent publish, sf agent preview, sf agent activate) replaces the click-heavy Setup workflow with real code — the agent's topics, instructions, and action wiring all live in version control next to the Apex class, as standard SFDX metadata. That means the agent is diffable, reviewable, and deployable in the same way as any other metadata. It also means I could preview agent behaviour from the terminal before activating, catching broken action wiring before anything touched production.

08 // Results

▸ live
Embedded on this siteClick the bubble in the bottom-right of any page. It's the real system, not a mockup.
▸ flow
End-to-end automationChat bubble click → Salesforce Lead → Agentforce follow-up, zero humans required.
▸ tests
150+ test functionsNode, tool, server, end-to-end, and eval tests, counted in the repo. CI runs the unit suite and recorded evals on every PR.
▸ cost
Scale-to-zero hostingAzure Container Apps free tier — the backend costs nothing when idle.
▸ flex
Provider-swappableClaude, GPT-4o, Llama (via Groq) or Grok — one env var plus that provider's API key, redeploy, done. Prod runs Grok (grok-4.3).
▸ ship
API-first designFully documented Swagger UI. Any frontend can consume the same backend.
fastapi · swagger · /docs
Swagger UI showing the FastAPI endpoints — /chat, /chat/stream, /chat/init, /health
// FastAPI Swagger UI — the backend exposes a clean REST surface, not just a widget endpoint

The backend API is publicly documented via Swagger UI — anyone can inspect the endpoints, try a /chat call directly, or wire the same backend into their own frontend. The chat widget on this site is just one consumer.

09 // What I Took From It

10 // Try It

This is a live demo — interactions create a real lead in Salesforce.

Click the chat bubble in the bottom-right of this page to talk to TARS. The source is public to read at github.com/marky224/salesforce-langgraph-ai-lead-bot under a proprietary, all-rights-reserved license — backend, eval suite, frontend widget, Salesforce metadata (the custom Lead fields), the Apex class, the Agent Script bundle, and the CI workflows. Backend runs on Azure Container Apps with scale-to-zero, so the first message may take 10–15 seconds to warm up. After that it's instant.