Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.maximem.ai/llms.txt

Use this file to discover all available pages before exploring further.

Status: Live in Playground · Try it: synap.maximem.ai/playground Open the playground and pick Salesforce — Enterprise Sales Assistant to see the reference implementation running.
A sales-rep-facing assistant that lives next to Salesforce. It knows the rep’s accounts, open opportunities, the deal narrative across recent activity, and what the rep has tried before. It drafts outreach, prepares for calls, logs activity, and updates the CRM — all while keeping the rep’s tone and territory context across sessions.

What you’ll build

A chat agent for sales reps that:
  • Pulls live CRM state — accounts, opps, contacts, recent activity
  • Remembers rep context — territory, ICP, tone of voice, deal-specific narratives
  • Drafts outreach in the rep’s voice, grounded in account history
  • Updates Salesforce — log calls, advance opp stages, create follow-up tasks
Est. build time: 45–60 minutes (most of that is Salesforce auth + field mapping).

When to use this recipe

Build this if:
  • Your reps work out of Salesforce and want an assistant that already knows their book of business
  • You want per-rep tone/persona memory (so drafts sound like the rep, not a generic LLM)
  • You need bi-directional CRM I/O (read accounts, write activities)
  • Account narratives span weeks and need to persist across sessions

Architecture at a glance

The rep is the user_id; the rep’s org is the customer_id. Account-specific notes get tagged in metadata so they’re recallable per-account.

Stack

LayerChoice
Synap SDKmaximem-synap (Python) / @maximem/synap (TypeScript)
FrameworkOpenAI Agents SDK (Python) / Vercel AI SDK (TypeScript)
CRMSalesforce REST + jsforce (Node) or simple-salesforce (Python)
LLMOpenAI gpt-4o
ChannelSidebar widget / Slack DM / web chat — choose what fits your reps

Prerequisites

  • A Synap API key — see Authentication
  • A Salesforce Connected App with OAuth + offline refresh tokens for the reps
  • Python: Python 3.11+
  • TypeScript: Node 18+ and Python 3.11+ on the host
TypeScript recipe runs on Node only. Pin Next.js route handlers to export const runtime = "nodejs". See Installation → JavaScript / TypeScript SDK.

Install

pip install maximem-synap maximem-synap-openai-agents openai-agents simple-salesforce

Configure

# .env
SYNAP_API_KEY=...
SYNAP_SERVER_URL=<maximem-server>
OPENAI_API_KEY=...
SALESFORCE_CLIENT_ID=...
SALESFORCE_CLIENT_SECRET=...

Build it

1. Identity & scoping

  • customer_id = <salesforce_org_id> — one tenant per Salesforce org
  • user_id = <salesforce_user_id> — the sales rep
  • conversation_id = <session UUID>
  • Account-scoped memories get account_id in metadata so you can retrieve “everything we know about Acme Corp” later
SESSIONS: dict[str, str] = {}

def conv_for(session_id: str) -> str:
    return SESSIONS.setdefault(session_id, str(uuid.uuid4()))

2. Salesforce tools

These wrap your Salesforce client. Authenticate per-rep using their stored refresh token before each call.
from agents import function_tool

@function_tool
async def get_account(account_id: str) -> dict:
    """Return account name, industry, ARR, owner, and open opp count."""
    sf = await get_sf_client_for_rep()
    return sf.Account.get(account_id)

@function_tool
async def get_opportunities(account_id: str, stage: str = None) -> list[dict]:
    """List open opps for an account, optionally filtered by stage."""
    sf = await get_sf_client_for_rep()
    soql = f"SELECT Id, Name, StageName, Amount, CloseDate FROM Opportunity WHERE AccountId = '{account_id}'"
    if stage: soql += f" AND StageName = '{stage}'"
    return sf.query(soql)["records"]

@function_tool
async def get_contacts(account_id: str) -> list[dict]:
    sf = await get_sf_client_for_rep()
    return sf.query(
        f"SELECT Id, Name, Title, Email FROM Contact WHERE AccountId = '{account_id}'"
    )["records"]

@function_tool
async def query_pipeline(stage: str = None, close_before: str = None) -> list[dict]:
    """Query the rep's own pipeline. Returns opps owned by the current rep."""
    sf = await get_sf_client_for_rep()
    soql = "SELECT Id, AccountId, Name, StageName, Amount, CloseDate FROM Opportunity WHERE OwnerId = '{me}'"
    if stage: soql += f" AND StageName = '{stage}'"
    if close_before: soql += f" AND CloseDate <= {close_before}"
    return sf.query(soql.format(me=sf.user_id))["records"]

@function_tool
async def log_activity(opportunity_id: str, subject: str, body: str) -> dict:
    sf = await get_sf_client_for_rep()
    return sf.Task.create({"WhatId": opportunity_id, "Subject": subject, "Description": body})

@function_tool
async def update_opportunity(opportunity_id: str, fields: dict) -> dict:
    sf = await get_sf_client_for_rep()
    return sf.Opportunity.update(opportunity_id, fields)

@function_tool
async def create_task(opportunity_id: str, subject: str, due_date: str) -> dict:
    sf = await get_sf_client_for_rep()
    return sf.Task.create({
        "WhatId": opportunity_id, "Subject": subject,
        "ActivityDate": due_date, "Status": "Open",
    })
The SOQL examples above use string interpolation for clarity. In production, parameterize all SOQL inputs to avoid injection — both simple-salesforce and jsforce support bind parameters.

3. System prompt

System prompt
You are a sales assistant embedded next to Salesforce. The user is a sales rep.

- Always check the rep's pipeline and the relevant account before suggesting actions.
- Use the rep's known tone, ICP framing, and prior account narratives from memory when drafting outreach.
- Never invent fields. If a value isn't in the CRM or memory, say so and ask.
- When asked to draft, return the draft text — do not auto-send.
- When asked to update the CRM, summarize the change and confirm before calling the update tool, unless the rep prefixes the request with "just" ("just log that call").
- Keep replies tight. Reps are busy.

4. Wire memory + LLM + tools

import os, uuid, asyncio
from agents import Agent, FunctionTool, Runner
from maximem_synap import MaximemSynapSDK
from synap_openai_agents import create_search_tool, create_store_tool

sdk = MaximemSynapSDK()
await sdk.initialize()

async def handle_message(rep_id: str, org_id: str, session_id: str, text: str) -> str:
    conv_id = conv_for(session_id)

    synap_search = create_search_tool(sdk=sdk, user_id=rep_id, customer_id=org_id)
    synap_store  = create_store_tool(sdk=sdk, user_id=rep_id, customer_id=org_id)

    agent = Agent(
        name="sf_sales_assistant",
        instructions=SYSTEM,
        tools=[
            FunctionTool(synap_search, name_override="synap_search"),
            FunctionTool(synap_store,  name_override="synap_store"),
            get_account, get_opportunities, get_contacts, query_pipeline,
            log_activity, update_opportunity, create_task,
        ],
    )

    result = await Runner.run(agent, input=text)
    reply = result.final_output

    asyncio.create_task(sdk.memories.create(
        document=f"Rep: {text}\nAssistant: {reply}",
        document_type="ai-chat-conversation",
        user_id=rep_id,
        customer_id=org_id,
        metadata={"conversation_id": conv_id, "channel": "sf-sidebar"},
    ))
    return reply

5. Tagging account-scoped memories

For richer recall (“what’s the latest on Acme?”), tag memories with the account when the conversation is about a specific account:
asyncio.create_task(sdk.memories.create(
    document=note_text,
    document_type="sales-note",
    user_id=rep_id,
    customer_id=org_id,
    metadata={"account_id": account_id, "opportunity_id": opp_id},
))
You can then filter retrieval by metadata.account_id for account-specific summarization.

Run & verify

Session 1
Rep:       I'm prepping for a call with Acme Corp tomorrow. What's the latest?
Assistant: Acme Corp — open opp "Acme Q2 Expansion", $120K, stage Proposal, close 2026-06-15.
           Last activity: Maya (champion, VP Eng) raised concerns about SSO timeline on 5/8.
           You drafted a response covering Q3 SSO availability but didn't send it.
           Want the draft text?

Rep:       Yes — and remind me Maya prefers Loom over Zoom for product walk-throughs.
Assistant: Noted (Maya — prefers Loom). Here's the draft: ...
Session 2 (next day, fresh conversation)
Rep:       Call with Maya done. Going to log it: we agreed on SSO by Q3, she's bringing security in next week.
Assistant: Logging on Acme Q2 Expansion: "Call with Maya — agreed SSO by Q3, security review next week."
           Want me to also create a follow-up task to prep the security review deck?

Rep:       Yes, due Friday.
Assistant: Task created on Acme Q2 Expansion, due Friday. (Also remembered: Maya prefers Loom for follow-ups.)
Memory carries across days. CRM stays the source of truth for structured deal state; Synap carries the narrative.

Customize / extend

  • Slack interface → wrap handle_message in a Slack bot. See Patterns → Slack Bot.
  • Replay historical activity on initial setup so the assistant has years of context from day one. See Patterns → Replay History.
  • Per-territory scoping → if a single rep covers multiple territories that shouldn’t share context, use metadata.territory to filter.
  • HubSpot / other CRMs → replace the Salesforce tool layer; the memory pattern is identical.
  • SDR variant → for top-of-funnel work, see AI SDR.

Troubleshooting

Account narrative goes missing across sessions
  • Confirm customer_id is the org ID, not the rep ID. Reps within the same org should see shared account context if you want that.
  • If you want strict per-rep silos, keep customer_id = org_id and rely on user_id for isolation — Synap scopes searches automatically.
Drafts don’t sound like the rep
  • The rep hasn’t given enough signal yet. Capture explicit corrections (“don’t open with ‘I hope this finds you well’”) with synap_store.
  • Or seed memory at onboarding with 5–10 of the rep’s recent sent emails as historical documents.
Tools timing out
  • Salesforce REST has per-org rate limits. Cache get_account and get_opportunities for the duration of one chat session.
TS route fails on Vercel
  • Pin export const runtime = "nodejs". JS SDK requires Node + Python on the host.