https://www.loom.com/share/5aeb45f472794102ae31ccb6a9ac84bb
A complete voice-powered Salesforce CRM integration for OpenHome that lets you search contacts, manage opportunities, log notes, create tasks, view pipeline summaries, and update deal stages β all by voice.
Salesforce is powerful but heavy. Updating a record means navigating to it, clicking Edit, changing a field, clicking Save. This ability collapses that to a sentence:
- "Log a note on Acme: they want to move forward" β Creates note instantly
- "How's my pipeline?" β Instant summary across all stages
- "Move the Acme deal to Closed Won" β Updates with one confirmation
| Mode | Voice Trigger | What It Does |
|---|---|---|
| 1. Search Contacts | "Look up Sarah Chen" | Search by name/email, speak details |
| 2. Search Opportunities | "How's the Acme deal?" | Search deals, speak stage/amount/date |
| 3. Log Note | "Log a note on Acme..." | Creates completed Task as note |
| 4. Create Task | "Create a task: send proposal by Friday" | Task with due date, priority, associations |
| 5. Pipeline Summary | "How's my pipeline?" | GROUP BY aggregation, totals by stage |
| 6. Move Stage | "Move Acme to Closed Won" | Update opportunity stage with confirmation |
- Prerequisites
- Setup Guide
- Installation
- All 6 Modes - Complete Guide
- Usage Examples
- Technical Details
- Troubleshooting
- API Reference
- β Salesforce account (Free Developer Edition works!)
- β Admin/Super Admin access (required to create Connected Apps)
- β PowerShell (for OAuth setup and testing)
- β 30 minutes for initial setup
If you don't have Salesforce:
- Go to: https://developer.salesforce.com/signup
- Fill out the form (use real email)
- Check your email and verify
- You'll get a Developer Edition org (free forever)
- Set your password and security question
Your org URL will be: https://yourorg-dev-ed.develop.my.salesforce.com
This is where OAuth credentials come from.
- Log in to Salesforce
- Click the βοΈ gear icon (top right)
- Click Setup
In Classic View:
- Quick Find:
Apps - Click Apps under Create
- Scroll to Connected Apps section
- Click New button
In Lightning View:
- Quick Find:
App Manager - Click App Manager
- Click New Connected App
| Field | Value |
|---|---|
| Connected App Name | OpenHome Voice Assistant |
| API Name | OpenHome_Voice_Assistant (auto-fills) |
| Contact Email | Your email |
β Check "Enable OAuth Settings"
Callback URL:
https://login.salesforce.com/services/oauth2/success
Selected OAuth Scopes - Add these:
- β Manage user data via APIs (api)
- β Perform requests at any time (refresh_token, offline_access)
β
Check "Require Secret for Web Server Flow"
β
Check "Require Secret for Refresh Token Flow"
β Uncheck "Require Proof Key for Code Exchange (PKCE)"
Click "Save"
"Your Connected App may take 2-10 minutes to be available."
β Take a break!
After 10 minutes:
- Go to Setup β Quick Find:
App Manager - Find OpenHome Voice Assistant
- Click the dropdown arrow (βΌ) β View
- Copy Consumer Key (starts with
3MVG9...) - Click "Click to reveal" β Copy Consumer Secret
Save both values!
Build the OAuth URL (replace YOUR_CONSUMER_KEY):
https://login.salesforce.com/services/oauth2/authorize?response_type=code&client_id=YOUR_CONSUMER_KEY&redirect_uri=https://login.salesforce.com/services/oauth2/success&scope=api%20refresh_token
Steps:
- Copy the complete URL (with your Consumer Key)
- Paste into your browser
- Log in to Salesforce (if needed)
- Click "Allow"
- You'll be redirected to:
https://login.salesforce.com/services/oauth2/success?code=aPrx... - Copy the code from the URL (everything after
code=)
Example:
URL: https://login.salesforce.com/services/oauth2/success?code=aPrxHJK9L3mN2p...
CODE: aPrxHJK9L3mN2p...
β° Act fast! Authorization codes expire in 90 seconds.
Open PowerShell and run (replace the values):
$body = @{
grant_type = "authorization_code"
code = "YOUR_AUTH_CODE"
client_id = "YOUR_CONSUMER_KEY"
client_secret = "YOUR_CONSUMER_SECRET"
redirect_uri = "https://login.salesforce.com/services/oauth2/success"
}
$response = Invoke-RestMethod -Uri "https://login.salesforce.com/services/oauth2/token" -Method POST -Body $body -ContentType "application/x-www-form-urlencoded"
$response | ConvertTo-JsonExpected Response:
{
"access_token": "00D5g000007Xxxx!ARoAQP...",
"refresh_token": "5Aep861W8Kh...",
"instance_url": "https://yourorg.my.salesforce.com",
"id": "https://login.salesforce.com/id/00D.../005...",
"token_type": "Bearer",
"issued_at": "1708300000000"
}From the response, save:
- β access_token - Short-lived (2 hours)
- β refresh_token - Long-lived (doesn't expire unless revoked)
- β instance_url - Your unique Salesforce org URL
Verify everything works:
$headers = @{
"Authorization" = "Bearer YOUR_ACCESS_TOKEN"
}
$uri = "YOUR_INSTANCE_URL/services/data/v62.0/query?q=SELECT+Id,Name+FROM+Account+LIMIT+5"
Invoke-RestMethod -Uri $uri -Headers $headers | ConvertTo-Jsonβ If you see account data, your API access is working!
Your Developer Edition comes with sample data, but let's add clean test data.
Empty the org:
$token = "YOUR_ACCESS_TOKEN"
$instance = "YOUR_INSTANCE_URL"
$headers = @{"Authorization" = "Bearer $token"}
# Get all opportunities
$uri = "$instance/services/data/v62.0/query?q=SELECT+Id,Name+FROM+Opportunity"
$result = Invoke-RestMethod -Uri $uri -Headers $headers
# Delete each one
foreach ($opp in $result.records) {
$deleteUri = "$instance/services/data/v62.0/sobjects/Opportunity/$($opp.Id)"
Invoke-RestMethod -Uri $deleteUri -Method DELETE -Headers $headers
}
Write-Host "Deleted! Now empty Recycle Bin in Salesforce UI."Then in Salesforce:
- App Launcher β Recycle Bin
- Click "Empty Recycle Bin"
Via Web UI:
- Go to Salesforce β Contacts tab β New
- Fill in:
- First Name: Sarah
- Last Name: Chen
- Account: Edge Communications
- Title: VP of Engineering
- Email: sarah@edge.com
- Phone: 555-1234
- Click Save
Via PowerShell:
$token = "YOUR_ACCESS_TOKEN"
$instance = "YOUR_INSTANCE_URL"
$headers = @{
"Authorization" = "Bearer $token"
"Content-Type" = "application/json"
}
# Get account IDs
$accounts = Invoke-RestMethod -Uri "$instance/services/data/v62.0/query?q=SELECT+Id,Name+FROM+Account+LIMIT+5" -Headers $headers
$edgeId = ($accounts.records | Where-Object {$_.Name -like "*Edge*"}).Id
# Create contact
$contact = @{
FirstName = "Sarah"
LastName = "Chen"
AccountId = $edgeId
Title = "VP of Engineering"
Email = "sarah@edge.com"
Phone = "555-1234"
} | ConvertTo-Json
Invoke-RestMethod -Uri "$instance/services/data/v62.0/sobjects/Contact" -Method POST -Headers $headers -Body $contact
Write-Host "β
Sarah Chen created!"Repeat for more contacts:
- John Miller at Burlington Textiles
- Emily Davis at Pyramid Construction
Via PowerShell:
# Create opportunity
$opp = @{
Name = "Acme Enterprise Deal"
AccountId = $edgeId
StageName = "Prospecting"
CloseDate = "2026-06-30"
Amount = 500000
} | ConvertTo-Json
Invoke-RestMethod -Uri "$instance/services/data/v62.0/sobjects/Opportunity" -Method POST -Headers $headers -Body $opp
Write-Host "β
Acme Enterprise Deal created!"Add 1-2 more opportunities for testing.
Open salesforce_main.py and update lines 18-22:
CONSUMER_KEY: ClassVar[str] = "YOUR_CONSUMER_KEY"
CONSUMER_SECRET: ClassVar[str] = "YOUR_CONSUMER_SECRET"
INSTANCE_URL: ClassVar[str] = "YOUR_INSTANCE_URL"
INITIAL_ACCESS_TOKEN: ClassVar[str] = "YOUR_ACCESS_TOKEN"
INITIAL_REFRESH_TOKEN: ClassVar[str] = "YOUR_REFRESH_TOKEN"Copy these files to your OpenHome abilities folder:
salesforce_main.pysalesforce__init__.py
Restart the OpenHome service/application.
Say: "Salesforce"
You should hear: "Salesforce Ready. What would you like to do?"
β If you hear that, installation is successful!
What It Does:
- Search contacts by name or email
- Speak back: name, title, company, email, phone
- Handle single/multiple/zero results
- Smart disambiguation
Voice Triggers:
- "Look up Sarah Chen"
- "Find john@acme.com"
- "Who is the VP at Edge Communications"
- "Search for Emily"
How It Works:
- Email Search (Exact Match):
SELECT Id, Name, Email, Phone, Title, Account.Name
FROM Contact
WHERE Email = 'sarah@edge.com'
LIMIT 5- Name Search (Fuzzy Match via SOSL):
FIND {Sarah Chen} IN NAME FIELDS
RETURNING Contact(Id, Name, Email, Phone, Title, Account.Name)
LIMIT 5
Response Patterns:
Single Result:
"I found Sarah Chen. She's the VP of Engineering at Edge Communications.
Email sarah@edge.com, phone 555-1234."
Multiple Results:
"I found 3 contacts. Sarah Chen, VP Engineering at Edge Communications.
Sarah Park, Director at Widget Co. Sarah Jones at TechCorp. Which one?"
User: "The first one"
App: [Shows full details for Sarah Chen]
Zero Results:
"I couldn't find any contacts matching that. Want me to search accounts instead?"
Test Commands:
"Salesforce"
"Look up Sarah Chen"
"Find john@edge.com"
"Search for Emily"
What It Does:
- Search opportunities by name
- Speak: name, stage, amount, close date, account, owner
- Currency formatting ("50 thousand dollars")
- Date formatting ("March 31st")
- Single/multiple/zero results
Voice Triggers:
- "How's the Acme deal?"
- "What's the status of the Enterprise opportunity?"
- "Check the Widget Co deal"
How It Works:
SELECT Id, Name, StageName, Amount, CloseDate,
Account.Name, Owner.FirstName, Owner.LastName
FROM Opportunity
WHERE Name LIKE '%Acme%'
AND IsClosed = false
LIMIT 5Response Pattern:
"The Acme Enterprise Deal opportunity is in Prospecting.
It's worth 500 thousand dollars. Close date: June 30th.
Account is Edge Communications. Owned by Haseeb."
Currency Formatting:
- $50,000 β "50 thousand dollars"
- $1,500,000 β "1.5 million dollars"
- $75,000 β "75 thousand dollars"
Date Formatting:
- 2026-03-15 β "March 15th"
- 2026-12-01 β "December 1st"
Test Commands:
"How's the Acme deal?"
"What's the status of the Enterprise opportunity?"
What It Does:
- Creates a completed Task as a note
- LLM parses target (contact/account/opportunity) and content
- Proper associations (WhoId/WhatId)
- No confirmation needed (speed!)
Voice Triggers:
- "Log a note on Acme: they want to move forward"
- "Add a note to Sarah Chen: she's interested in the demo"
- "Note for the Enterprise deal: discussed pricing"
How It Works:
- Parse Command:
Input: "log a note on Acme: they want to move forward with enterprise"
Output: {
"target": "Acme",
"content": "they want to move forward with enterprise"
}
- Find Target:
- Searches Contacts for "Acme"
- If not found, searches Accounts for "Acme"
- If not found, searches Opportunities for "Acme"
- Create Task:
POST /sobjects/Task
{
"Subject": "Voice Note: they want to move forward with enterprise",
"Description": "Captured via OpenHome voice: they want to move forward with enterprise",
"Status": "Completed",
"Priority": "Normal",
"ActivityDate": "2026-03-05",
"WhatId": "001XXXX..." // Account ID
}Association Logic:
| Target Type | Field Used |
|---|---|
| Contact | WhoId |
| Account | WhatId |
| Opportunity | WhatId |
Response:
"Done. I've logged a note on Acme: they want to move forward with enterprise"
Verification:
- Go to Salesforce β Accounts β Acme
- Scroll to Activity timeline
- See the completed Task with your note
Test Commands:
"Log a note on Edge Communications: they want to move forward"
"Add a note to Sarah Chen: she's interested in the demo"
What It Does:
- Create tasks with due date and priority
- Natural language date parsing
- Optional target (contact/account/opportunity)
- Smart defaults (tomorrow, Normal priority)
Voice Triggers:
- "Create a task: send proposal to Acme by Friday"
- "Remind me to follow up with Sarah tomorrow"
- "Task for the Enterprise deal: schedule demo, high priority"
How It Works:
- Parse Command:
Input: "create a task: send proposal to Acme by Friday"
Output: {
"subject": "send proposal to Acme",
"due_date": "Friday",
"priority": "Normal",
"target": "Acme"
}
- Parse Due Date:
"tomorrow" β 2026-03-06
"Friday" β 2026-03-07 (this Friday)
"next Monday" β 2026-03-10
"today" β 2026-03-05
(no date) β Tomorrow (default)
- Create Task:
POST /sobjects/Task
{
"Subject": "send proposal to Acme",
"Description": "Voice-created via OpenHome",
"ActivityDate": "2026-03-07",
"Status": "Not Started",
"Priority": "Normal",
"WhatId": "001XXXX..."
}Date Parsing:
| User Says | Parsed To |
|---|---|
| "tomorrow" | Tomorrow |
| "today" | Today |
| "Friday" | This/Next Friday |
| "next Monday" | Next Monday |
| "Tuesday" | This/Next Tuesday |
| (no date) | Tomorrow (default) |
Priority Levels:
| User Says | Salesforce Value |
|---|---|
| "high priority" | High |
| "low priority" | Low |
| (nothing) | Normal (default) |
Response:
"Done. I've created a task: send proposal to Acme, due Friday, for Acme."
Test Commands:
"Create a task: send proposal to Acme by Friday"
"Remind me to follow up with Sarah tomorrow"
"Task for Enterprise: schedule demo, high priority"
What It Does:
- SOQL GROUP BY aggregation (server-side!)
- Total deal count and value
- Breakdown by stage with counts and values
- Sorted by value (highest first)
- Speaks top 6 stages
Voice Triggers:
- "How's my pipeline?"
- "What opportunities do I have open?"
- "Show my pipeline"
- "How many deals do I have?"
How It Works:
- Query Totals:
SELECT COUNT(Id) cnt, SUM(Amount) total
FROM Opportunity
WHERE IsClosed = false- Query Breakdown:
SELECT StageName, COUNT(Id) cnt, SUM(Amount) total
FROM Opportunity
WHERE IsClosed = false
GROUP BY StageName
ORDER BY SUM(Amount) DESCResponse:
"Here's your pipeline. You have 3 open opportunities worth 1.1 million dollars total.
2 deals in Prospecting worth 750 thousand dollars.
1 deal in Qualification worth 350 thousand dollars."
Key Advantage Over HubSpot:
HubSpot: Must fetch ALL deals client-side, then aggregate in Python
Salesforce: Single query, server-side aggregation with GROUP BY β
Test Commands:
"How's my pipeline?"
"What opportunities do I have open?"
What It Does:
- Update opportunity stage
- Smart stage matching with aliases
- CONFIRMATION REQUIRED (safety!)
- Shows current β target stage
- Speech recognition aliases ("closed one" β "closed won")
Voice Triggers:
- "Move the Acme deal to Qualification"
- "Update Enterprise to Closed Won"
- "Mark the Widget opportunity as Closed Lost"
How It Works:
- Parse Command:
Input: "move the Acme deal to closed won"
Output: {
"opportunity_name": "Acme",
"target_stage": "closed won"
}
- Find Opportunity:
SELECT Id, Name, StageName
FROM Opportunity
WHERE Name LIKE '%Acme%'
LIMIT 1- Match Stage Name:
User says: "closed won"
Matches to: "Closed Won" (exact)
User says: "won"
Alias matches to: "Closed Won"
User says: "qualification"
Matches to: "Qualification"
- Confirm:
"I'll move the Acme Enterprise Deal from Prospecting to Closed Won. Confirm?"
User: "Yes"
- Update:
PATCH /sobjects/Opportunity/006XXX...
{
"StageName": "Closed Won"
}Stage Aliases:
| User Says | Matches To |
|---|---|
| "won" | Closed Won |
| "we won it" | Closed Won |
| "closed one" | Closed Won (STT fix) |
| "lost" | Closed Lost |
| "dead" | Closed Lost |
| "qualified" | Qualification |
| "proposal" | Proposal/Price Quote |
| "negotiation" | Negotiation/Review |
| "value prop" | Value Proposition |
Response:
"I'll move the Acme Enterprise Deal from Prospecting to Closed Won. Confirm?"
[User says "Yes"]
"Done. I've moved Acme Enterprise Deal to Closed Won."
Test Commands:
"Move the Acme deal to Qualification"
"Update Enterprise to Closed Won"
"Mark Acme as Closed Lost"
User: "Salesforce"
App: "Salesforce Ready. What would you like to do?"
User: "Look up Sarah Chen"
App: "Searching for contacts..."
App: "I found Sarah Chen. She's the VP of Engineering at Edge Communications.
Email sarah@edge.com, phone 555-1234."
User: "How's the Acme deal?"
App: "Searching for opportunities..."
App: "The Acme Enterprise Deal opportunity is in Prospecting.
It's worth 500 thousand dollars. Close date: June 30th.
Account is Edge Communications. Owned by Haseeb."
User: "Log a note on it: they want to move forward with the enterprise plan"
App: "Logging a note..."
App: "Done. I've logged a note on Acme Enterprise Deal:
they want to move forward with the enterprise plan"
User: "Create a task: send proposal by Friday, high priority"
App: "Creating a task..."
App: "Done. I've created a task: send proposal, due Friday, high priority."
User: "How's my pipeline?"
App: "Getting your pipeline summary..."
App: "Here's your pipeline. You have 2 open opportunities worth 1.1 million dollars total.
2 deals in Prospecting worth 1.1 million dollars."
User: "Move the Acme deal to Qualification"
App: "Updating opportunity stage..."
App: "I'll move the Acme Enterprise Deal from Prospecting to Qualification. Confirm?"
User: "Yes"
App: "Done. I've moved Acme Enterprise Deal to Qualification."
User: "Done"
App: "Goodbye."
OAuth 2.0 Flow:
- User creates Connected App
- Browser-based consent flow
- Exchange authorization code for tokens
- Auto-refresh every 2 hours
- Refresh token reused (never expires unless revoked)
Token Refresh:
# Checked before every API call
if token_age > 7200: # 2 hours
refresh_access_token()API Request Pattern:
async def sf_request(method, path, data=None):
# 1. Check token age, refresh if needed
# 2. Make request with Authorization header
# 3. Handle 401 with one retry after refresh
# 4. Return resultSOQL (SQL-like queries):
- Exact field matching
- Aggregation (COUNT, SUM, GROUP BY)
- Relationship traversal (Account.Name)
- Used for: opportunities, pipeline summary, exact email
SOSL (Full-text search):
- Fuzzy matching across multiple objects
- Better for partial names
- Used for: contact name search
Salesforce accepts minimal encoding:
# Only encode spaces as +
encoded = soql.replace(" ", "+")
# Leave commas, quotes, parentheses unencodedWhoId vs WhatId:
WhoIdβ Contacts and Leads (people)WhatIdβ Accounts, Opportunities, other objects (things)
Example:
{
"WhoId": "003XXX...", // Sarah Chen (Contact)
"WhatId": "001XXX..." // Edge Communications (Account)
}def parse_due_date(text):
if "tomorrow" in text:
return (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d")
elif "friday" in text:
days_ahead = (4 - datetime.now().weekday()) % 7
if days_ahead == 0:
days_ahead = 7
return (datetime.now() + timedelta(days=days_ahead)).strftime("%Y-%m-%d")
# ... more patternsdef format_currency(amount):
if amount >= 1000000:
return f"{amount/1000000:.1f} million dollars"
elif amount >= 1000:
return f"{amount/1000:.0f} thousand dollars"
else:
return f"{amount} dollars"Cause: Connected App not propagated yet
Fix: Wait 10 minutes after creating Connected App
Cause: Access token expired
Fix: Token auto-refreshes. If it persists, re-authenticate:
# Refresh token manually
$body = @{
grant_type = "refresh_token"
refresh_token = "YOUR_REFRESH_TOKEN"
client_id = "YOUR_CONSUMER_KEY"
client_secret = "YOUR_CONSUMER_SECRET"
}
$response = Invoke-RestMethod -Uri "https://login.salesforce.com/services/oauth2/token" -Method POST -Body $body -ContentType "application/x-www-form-urlencoded"
# Update access_token in main.pyCause: URL encoding issue
Fix: Already fixed in code (only encodes spaces as +)
Cause: Sample data in Developer Edition
Fix: Delete all opportunities and recreate:
# Get all opportunities
$uri = "$instance/services/data/v62.0/query?q=SELECT+Id+FROM+Opportunity"
$result = Invoke-RestMethod -Uri $uri -Headers @{"Authorization"="Bearer $token"}
# Delete each
foreach ($opp in $result.records) {
Invoke-RestMethod -Uri "$instance/services/data/v62.0/sobjects/Opportunity/$($opp.Id)" -Method DELETE -Headers @{"Authorization"="Bearer $token"}
}
# Empty Recycle Bin in Salesforce UICause: No test data in org
Fix: Add test data (see Step 4 in Setup)
Cause: Stage name different in your org
Fix: Check available stages:
$uri = "$instance/services/data/v62.0/query?q=SELECT+MasterLabel+FROM+OpportunityStage+WHERE+IsActive=true"
Invoke-RestMethod -Uri $uri -Headers @{"Authorization"="Bearer $token"}Add custom aliases in match_stage_name() function.
Base URL: https://{instance_url}/services/data/v62.0/
Authentication:
Authorization: Bearer {access_token}
GET /query?q={SOQL_QUERY}
Example:
GET /query?q=SELECT+Id,Name+FROM+Contact+LIMIT+5
GET /search?q={SOSL_QUERY}
Example:
GET /search?q=FIND+{Sarah}+IN+NAME+FIELDS+RETURNING+Contact(Id,Name)
POST /sobjects/{Object}
Content-Type: application/json
Body: {field1: value1, field2: value2}
Example:
POST /sobjects/Task
{"Subject": "Follow up", "Status": "Not Started"}
PATCH /sobjects/{Object}/{Id}
Content-Type: application/json
Body: {field1: new_value}
Example:
PATCH /sobjects/Opportunity/006XXX
{"StageName": "Closed Won"}
DELETE /sobjects/{Object}/{Id}
Example:
DELETE /sobjects/Opportunity/006XXX
SELECT Id, Name, Email, Phone, Title, Account.Name
FROM ContactFields:
Id- Salesforce ID (18 chars)Name- Full nameEmail- Email addressPhone- Phone numberTitle- Job titleAccountId- Related Account IDAccount.Name- Related Account name (relationship)
SELECT Id, Name, StageName, Amount, CloseDate,
Account.Name, Owner.FirstName, Owner.LastName
FROM OpportunityFields:
Id- Salesforce IDName- Opportunity nameStageName- Current stage (human-readable)Amount- Deal valueCloseDate- Expected close date (YYYY-MM-DD)IsClosed- Boolean (true if won or lost)IsWon- Boolean (true if won)AccountId- Related Account IDOwnerId- Owner user ID
Standard Stages:
- Prospecting
- Qualification
- Needs Analysis
- Value Proposition
- Id. Decision Makers
- Perception Analysis
- Proposal/Price Quote
- Negotiation/Review
- Closed Won
- Closed Lost
SELECT Id, Name, Phone, Industry, BillingCity
FROM AccountFields:
Id- Salesforce IDName- Company namePhone- Company phoneIndustry- Industry categoryBillingCity,BillingState- Address
SELECT Id, Subject, Status, Priority, ActivityDate
FROM TaskFields:
Id- Salesforce IDSubject- Task title (max 255 chars)Description- Full descriptionStatus- Not Started, In Progress, Completed, DeferredPriority- High, Normal, LowActivityDate- Due date (DATE only, no time)WhoId- Related Contact/LeadWhatId- Related Account/Opportunity
-- Exact match
WHERE Email = 'sarah@acme.com'
-- Partial match
WHERE Name LIKE '%Sarah%'
-- NOT equal
WHERE StageName != 'Closed Won'
-- Boolean
WHERE IsClosed = false
-- IN list
WHERE StageName IN ('Prospecting', 'Qualification')
-- NOT IN list
WHERE StageName NOT IN ('Closed Won', 'Closed Lost')
-- Aggregation
SELECT COUNT(Id), SUM(Amount)
GROUP BY StageName
-- Order
ORDER BY Amount DESC
-- Limit
LIMIT 10Enterprise Edition:
- 100,000 API calls per 24 hours (org-wide)
- 1,000 API calls per user license per 24 hours
- No per-second throttle
Developer Edition:
- 15,000 API calls per 24 hours
Check Usage:
GET /limits
-
Never commit credentials to git
# Add to .gitignore *_prefs.json salesforce_main.py # If it contains hardcoded tokens
-
Use environment variables
import os CONSUMER_KEY = os.getenv("SF_CONSUMER_KEY") CONSUMER_SECRET = os.getenv("SF_CONSUMER_SECRET")
-
Rotate tokens regularly
- Revoke old Connected Apps
- Create new ones every 90 days
-
Use HTTPS only
- Never send tokens over HTTP
- All Salesforce endpoints use HTTPS
-
Minimum scopes
- Only request
apiandrefresh_token - Don't request
fullaccess unless needed
- Only request
Current (V1): Hardcoded in main.py -
Production (V2): Encrypted storage with key rotation
Implementation:
- ~1,800 lines of code
- 6 complete modes
- OAuth 2.0 with auto-refresh
- SOQL and SOSL queries
- Natural language parsing
- Date and currency formatting
- Smart disambiguation
- Error handling
API Calls per Mode:
| Mode | API Calls |
|---|---|
| Search Contacts | 1 (SOSL) or 1 (SOQL) |
| Search Opportunities | 1 (SOQL) |
| Log Note | 1-3 (search) + 1 (create) |
| Create Task | 0-3 (search) + 1 (create) |
| Pipeline Summary | 2 (totals + breakdown) |
| Move Stage | 1 (search) + 1 (update) |
π SEARCH
"Look up Sarah Chen"
"How's the Acme deal?"
π NOTES & TASKS
"Log a note on Acme: they want to move forward"
"Create a task: send proposal by Friday"
π PIPELINE
"How's my pipeline?"
π― UPDATES
"Move Acme to Closed Won"
β EXIT
"Done" or "Goodbye"
| Issue | Fix |
|---|---|
| 401 error | Token expired, auto-refreshes |
| Can't find contact | Add test data |
| Wrong pipeline count | Delete sample data |
| Stage not matching | Check available stages in org |
Test the complete flow:
- Say "Salesforce"
- Try each mode
- Verify in Salesforce UI
Need help?
- Check logs for detailed error messages
- Verify API credentials
- Ensure test data exists
- Check Salesforce permissions
Version 1.0 (March 2026)
- β All 6 modes implemented
- β OAuth 2.0 with token refresh
- β SOQL and SOSL queries
- β Smart disambiguation
- β Natural language parsing
- β Error handling
- β Production-ready
Enjoy your voice-powered Salesforce CRM! π