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  (GitHub Pages · 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)
      ↓  ainvoke()
LLM Provider  (Claude / GPT-4o / Llama / Grok — 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 on GitHub Pages 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.
▸ 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.
▸ 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 router recognises that and skips ahead. 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 // 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 next to the Apex classes in a standard SFDX project. 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.

07 // 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
63 passing testsNode, tool, and end-to-end conversation tests run in under 2 seconds.
▸ cost
Scale-to-zero hostingAzure Container Apps free tier — the backend costs nothing when idle.
▸ flex
Provider-swappableClaude, GPT-4o, Llama, Grok — change one env var, redeploy, done.
▸ 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.

08 // What I Took From It

09 // 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. Full source is open at github.com/marky224/salesforce-langgraph-ai-lead-bot — backend, frontend widget, Salesforce metadata, Apex classes, Agent Script bundle, and deployment guides all included. 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.