AI Support Guide

Salesforce Service Cloud

Connect Simpli services to Salesforce Service Cloud for AI-powered case handling.

Salesforce Service Cloud uses Cases rather than tickets, and offers deep customization through custom objects, fields, and automation. This guide walks through connecting every Simpli service to Service Cloud using Platform Events, middleware, and the Salesforce REST API.

Salesforce remains your system of record. Simpli adds AI capabilities alongside it — auto-classifying cases, drafting responses, scoring conversations, and tracking customer health.

Prerequisites

  • Salesforce Service Cloud (Enterprise edition or higher recommended for Platform Events)
  • A Connected App configured for OAuth 2.0 — create one in Setup > App Manager > New Connected App
  • API access enabled for your Salesforce user profile
  • AI capabilities deployed and running
  • A middleware endpoint — your own server or serverless function that sits between Salesforce and Simpli

Architecture

There are two approaches for connecting Salesforce to Simpli. Platform Events are recommended for most deployments.

┌───────────┐   Platform Event    ┌──────────────┐     REST API      ┌─────────────────┐
│ Salesforce │ ────────────────► │  Middleware   │ ────────────────► │ Simpli Services  │
│  Service   │ ◄──────────────── │  (your code)  │ ◄──────────────── │ Triage, Reply... │
│  Cloud     │   Salesforce API   └──────────────┘    AI results      └─────────────────┘
└───────────┘

An Apex trigger publishes a Platform Event when a Case is created or updated. Your middleware subscribes to the event stream via CometD or gRPC, calls Simpli, and writes results back via the Salesforce REST API.

Approach 2: Apex HTTP callouts

Apex triggers call your middleware directly using Named Credentials. This is simpler but less decoupled — if your middleware is down, the Apex callout fails and the transaction may roll back.

Authentication

Use the simple-salesforce Python library with OAuth 2.0 client credentials:

from simple_salesforce import Salesforce

sf = Salesforce(
    username="integration@yourcompany.com",
    password="your-password",
    security_token="your-security-token",
    domain="login",  # Use "test" for sandboxes
)

# Or use OAuth client credentials flow
sf = Salesforce(
    instance_url="https://yourcompany.my.salesforce.com",
    session_id="your-oauth-access-token",
)

For middleware that receives Platform Events, you will also need the CometD streaming endpoint. All API examples below use sf as the Salesforce client and httpx for Simpli calls.

Custom fields and objects

Before integrating, create these custom fields in Salesforce Setup:

On the Case object

Field labelAPI nameTypePurpose
AI CategoryAI_Category__cPicklistTriage classification category
AI UrgencyAI_Urgency__cPicklistUrgency level from Triage
AI ConfidenceAI_Confidence__cPercentClassification confidence score
QA ScoreQA_Score__cPercentOverall QA evaluation score

On the Contact object

Field labelAPI nameTypePurpose
Sentiment ScoreSentiment_Score__cTextLatest sentiment label
Escalation RiskEscalation_Risk__cNumber(3,2)Risk score from 0 to 1

Custom object: QA Evaluation

Create a custom object QA_Evaluation__c for detailed QA results:

Field labelAPI nameType
CaseCase__cLookup(Case)
AgentAgent__cLookup(User)
Overall ScoreOverall_Score__cPercent
EmpathyEmpathy__cPercent
AccuracyAccuracy__cPercent
ResolutionResolution__cPercent
Coaching NotesCoaching_Notes__cLong Text Area

Per-service integration

Triage — auto-classify and route cases

When: A new Case is created.

Flow: A Platform Event fires on Case creation. Middleware subscribes to the event, calls Triage /classify and /route, then updates the Case with custom field values and reassigns it.

import httpx

TRIAGE_URL = "http://localhost:8001"

async def classify_and_route(case_id: str, subject: str, description: str):
    async with httpx.AsyncClient() as client:
        # Classify the case
        classify_resp = await client.post(f"{TRIAGE_URL}/classify", json={
            "subject": subject,
            "body": description,
        })
        classification = classify_resp.json()

        # Route based on classification
        route_resp = await client.post(f"{TRIAGE_URL}/route", json={
            "ticket_id": case_id,
            "category": classification["category"],
            "urgency": classification["urgency"],
            "sentiment": classification["sentiment"],
        })
        routing = route_resp.json()

    # Map urgency to Salesforce Priority
    priority_map = {"low": "Low", "medium": "Medium", "high": "High", "critical": "Critical"}

    # Update the Case in Salesforce
    sf.Case.update(case_id, {
        "AI_Category__c": classification["category"],
        "AI_Urgency__c": classification["urgency"],
        "AI_Confidence__c": classification["confidence"] * 100,  # Percent field
        "Priority": priority_map.get(classification["urgency"], "Medium"),
        "OwnerId": routing.get("group_id"),  # Queue or User ID
    })

    return {"classification": classification, "routing": routing}

Salesforce picklist values must be pre-configured. Add values like billing, technical, account, feature_request, general to the AI_Category__c field.

Reply — AI draft responses

When: After Triage classifies a case (or on demand).

Flow: Middleware calls Reply /api/v1/draft and creates a CaseComment with the draft. The agent reviews it before sending a reply through their preferred channel.

REPLY_URL = "http://localhost:8002"

async def draft_reply(case_id: str, conversation: list[dict]):
    async with httpx.AsyncClient() as client:
        # Generate a draft
        draft_resp = await client.post(f"{REPLY_URL}/api/v1/draft", json={
            "ticket_id": case_id,
            "conversation": conversation,
            "style": "friendly",
            "language": "en",
        })
        draft = draft_resp.json()

    # Post as internal CaseComment
    sf.CaseComment.create({
        "ParentId": case_id,
        "CommentBody": (
            f"[AI Draft Response] (confidence: {draft['confidence']:.0%})\n\n"
            f"{draft['draft']}\n\n"
            f"--- Copy and edit this draft before sending. ---"
        ),
        "IsPublished": False,  # Internal only
    })

    return draft

Alternatively, create an EmailMessage draft if the case originated via Email-to-Case:

sf.EmailMessage.create({
    "ParentId": case_id,
    "Subject": f"Re: {subject}",
    "TextBody": draft["draft"],
    "Status": "5",  # Draft status
    "Incoming": False,
})

Sentiment — customer health tracking

When: A new CaseComment is added by the customer.

Flow: Middleware analyzes the comment text and updates custom fields on the Contact record.

SENTIMENT_URL = "http://localhost:8004"

async def analyze_comment(case_id: str, contact_id: str, comment_text: str):
    async with httpx.AsyncClient() as client:
        sentiment_resp = await client.post(f"{SENTIMENT_URL}/analyze", json={
            "customer_id": contact_id,
            "text": comment_text,
            "channel": "web",
        })
        sentiment = sentiment_resp.json()

    # Update Contact custom fields
    sf.Contact.update(contact_id, {
        "Sentiment_Score__c": sentiment["label"],
        "Escalation_Risk__c": round(sentiment["escalation_risk"], 2),
    })

    # If high escalation risk, also flag the Case
    if sentiment["escalation_risk"] >= 0.5:
        sf.Case.update(case_id, {
            "Priority": "Critical",
        })

    return sentiment

Create a Salesforce Report or Dashboard that filters Contacts by Escalation_Risk__c > 0.5 to surface at-risk customers.

QA — conversation scoring

When: A Case status changes to Closed.

Flow: Middleware pulls CaseComments, sends them to QA /evaluate, and creates a QA_Evaluation__c record linked to the Case.

QA_URL = "http://localhost:8003"

async def score_conversation(case_id: str):
    # Pull CaseComments from Salesforce
    result = sf.query(
        f"SELECT Id, CommentBody, CreatedById, CreatedBy.Profile.Name "
        f"FROM CaseComment WHERE ParentId = '{case_id}' ORDER BY CreatedDate ASC"
    )

    messages = []
    agent_id = ""
    for record in result["records"]:
        # Determine role based on whether the commenter is an internal user
        is_agent = record.get("CreatedBy", {}).get("Profile", {}).get("Name") != "Customer Community User"
        role = "agent" if is_agent else "customer"
        messages.append({"role": role, "content": record["CommentBody"]})
        if is_agent:
            agent_id = record["CreatedById"]

    async with httpx.AsyncClient() as client:
        qa_resp = await client.post(f"{QA_URL}/evaluate", json={
            "conversation_id": case_id,
            "agent_id": agent_id,
            "messages": messages,
        })
        evaluation = qa_resp.json()

    # Update the QA Score on the Case
    sf.Case.update(case_id, {
        "QA_Score__c": evaluation["overall_score"] * 100,
    })

    # Create a detailed QA Evaluation record
    dimensions = evaluation.get("dimensions", {})
    sf.QA_Evaluation__c.create({
        "Case__c": case_id,
        "Agent__c": agent_id,
        "Overall_Score__c": evaluation["overall_score"] * 100,
        "Empathy__c": dimensions.get("empathy", 0) * 100,
        "Accuracy__c": dimensions.get("accuracy", 0) * 100,
        "Resolution__c": dimensions.get("resolution", 0) * 100,
        "Coaching_Notes__c": "\n".join(evaluation.get("coaching_notes", [])),
    })

    return evaluation

Using a custom object for QA results lets you build Salesforce Reports with agent scorecards, trend analysis, and coaching dashboards — all native to the Salesforce UI.

KB — Salesforce Knowledge article sync

Sync Salesforce Knowledge articles into Simpli KB. Requires Salesforce Knowledge to be enabled.

KB_URL = "http://localhost:8006"

async def sync_salesforce_knowledge():
    # Query published Knowledge articles
    result = sf.query(
        "SELECT Id, Title, Summary, ArticleBody "
        "FROM Knowledge__kav "
        "WHERE PublishStatus = 'Online' AND Language = 'en_US'"
    )

    async with httpx.AsyncClient() as client:
        for article in result["records"]:
            await client.post(f"{KB_URL}/articles", json={
                "title": article["Title"],
                "content": article.get("ArticleBody", article.get("Summary", "")),
                "tags": [],
                "source": "salesforce-knowledge",
            })

        # Handle pagination
        while not result["done"]:
            result = sf.query_more(result["nextRecordsUrl"], identifier_is_url=True)
            for article in result["records"]:
                await client.post(f"{KB_URL}/articles", json={
                    "title": article["Title"],
                    "content": article.get("ArticleBody", article.get("Summary", "")),
                    "tags": [],
                    "source": "salesforce-knowledge",
                })

        # Check for content gaps
        gaps_resp = await client.get(f"{KB_URL}/gaps")
        return gaps_resp.json()

The Knowledge article object API name depends on your configuration. Common names are Knowledge__kav or a custom article type like FAQ__kav. Check your Setup to confirm.

Platform Event setup

1. Create the Platform Event

In Setup > Platform Events, create a new event:

  • Label: Simpli Case Event
  • API Name: Simpli_Case_Event__e
  • Fields: Case_Id__c (Text), Event_Type__c (Text), Subject__c (Text), Description__c (Long Text Area)

2. Create the Apex trigger

trigger CaseSimpleTrigger on Case (after insert, after update) {
    List<Simpli_Case_Event__e> events = new List<Simpli_Case_Event__e>();

    for (Case c : Trigger.new) {
        if (Trigger.isInsert) {
            events.add(new Simpli_Case_Event__e(
                Case_Id__c = c.Id,
                Event_Type__c = 'created',
                Subject__c = c.Subject,
                Description__c = c.Description
            ));
        } else if (Trigger.isUpdate) {
            Case oldCase = Trigger.oldMap.get(c.Id);
            if (c.Status == 'Closed' && oldCase.Status != 'Closed') {
                events.add(new Simpli_Case_Event__e(
                    Case_Id__c = c.Id,
                    Event_Type__c = 'closed',
                    Subject__c = c.Subject,
                    Description__c = c.Description
                ));
            }
        }
    }

    if (!events.isEmpty()) {
        EventBus.publish(events);
    }
}

3. Subscribe from middleware

Use CometD (via python-bayeux or aiosfstream) to subscribe to the Platform Event stream:

from aiosfstream import SalesforceStreamingClient

async def listen_for_events():
    async with SalesforceStreamingClient(
        consumer_key="your-connected-app-key",
        consumer_secret="your-connected-app-secret",
        username="integration@yourcompany.com",
        password="your-password-and-token",
    ) as client:
        await client.subscribe("/event/Simpli_Case_Event__e")

        async for message in client:
            event = message["data"]["payload"]
            case_id = event["Case_Id__c"]
            event_type = event["Event_Type__c"]

            if event_type == "created":
                await classify_and_route(
                    case_id,
                    event["Subject__c"],
                    event["Description__c"],
                )
            elif event_type == "closed":
                await score_conversation(case_id)

Enterprise considerations

  • OAuth flow: Use the JWT Bearer flow for server-to-server integration. Avoid storing passwords — use a certificate-based Connected App.
  • Governor limits: Apex triggers are subject to limits (100 SOQL queries per transaction, 150 DML operations). Platform Events avoid this by decoupling the callout from the transaction.
  • Salesforce Shield: If your org uses Shield Platform Encryption, ensure custom fields storing AI results are marked as encrypted if they contain sensitive data.
  • Sandboxes: Always test in a sandbox first. Use domain="test" in the simple-salesforce constructor for sandbox orgs.
  • Named Credentials: If using Apex callouts instead of Platform Events, set up Named Credentials in Setup to manage authentication to your middleware endpoint.

Rate limits and best practices

Salesforce API limits depend on your edition and license count. Enterprise edition typically allows 100,000 API calls per 24 hours.

  • Monitor API usage. Check Setup > System Overview for current API consumption.
  • Use bulk operations. When syncing KB articles, batch them rather than making individual API calls.
  • Handle API errors gracefully. Salesforce returns detailed error responses — log the errorCode and message fields.
  • Keep middleware idempotent. Platform Events provide replay IDs — use them to avoid reprocessing events after a restart.

Next steps

On this page