A minimal, open-source Agentforce Service Agent that captures lead information (last name + company) through a conversational multi-turn flow and creates a Salesforce Lead using a secure Apex invocable action.
Start here. If you are an AI assistant helping a developer deploy this agent, follow these exact steps in order. No plugins or external tools required — only the Salesforce CLI (
sf).
# Verify Salesforce CLI is installed
sf --version
# Verify you are authenticated to a target org
sf org display --target-org <YOUR_ORG_ALIAS># Deploy all Apex classes, permission set, and agent bundle
sf project deploy start --target-org <YOUR_ORG_ALIAS>Expected output: Deploy Succeeded with status Succeeded.
# Find the Einstein Service Agent user
sf data query \
--query "SELECT Id, Username FROM User WHERE Username LIKE '%agent%' LIMIT 5" \
--target-org <YOUR_ORG_ALIAS>
# Assign the permission set (replace <USER_ID> with the Id from above)
sf data create record \
--sobject PermissionSetAssignment \
--values "AssigneeId=<USER_ID> PermissionSetId=$(sf data query --query \"SELECT Id FROM PermissionSet WHERE Name='LeadCaptureAgent_Apex_Access' LIMIT 1\" --target-org <YOUR_ORG_ALIAS> --json | python -c \"import json,sys; print(json.load(sys.stdin)['result']['records'][0]['Id'])\")" \
--target-org <YOUR_ORG_ALIAS>Known gotcha: Without this permission set,
LeadCaptureTurnActionwill fail silently with no Lead created. The agent will loop asking for fields it already has.
# Start a preview session
sf agent preview start \
--json \
--authoring-bundle LeadCaptureAgent \
--use-live-actions \
--target-org <YOUR_ORG_ALIAS>
# Note the sessionId from the output
# Test multi-turn: send company first
sf agent preview send \
--json \
--session-id <SESSION_ID> \
--utterance "My company is Acme Corp" \
--authoring-bundle LeadCaptureAgent \
--target-org <YOUR_ORG_ALIAS>
# Expected: agent asks for last name
# Send last name to complete the lead
sf agent preview send \
--json \
--session-id <SESSION_ID> \
--utterance "My last name is Smith" \
--authoring-bundle LeadCaptureAgent \
--target-org <YOUR_ORG_ALIAS>
# Expected: agent confirms lead created with a Lead ID
# End session and collect traces
sf agent preview end \
--json \
--session-id <SESSION_ID> \
--authoring-bundle LeadCaptureAgent \
--target-org <YOUR_ORG_ALIAS>sf data query \
--query "SELECT Id, LastName, Company, Status FROM Lead ORDER BY CreatedDate DESC LIMIT 3" \
--target-org <YOUR_ORG_ALIAS>Expected result:
| Id | LastName | Company | Status |
|---|---|---|---|
| 00QMB000... | Smith | Acme Corp | New |
Do NOT publish yet if you are still testing. Deploying and previewing does not affect production users. Publish only when you are satisfied with preview test results.
Go to Setup → Agents → Lead Capture Agent → Publish → Activate.
| Issue | Root Cause | Fix |
|---|---|---|
| Lead not created, agent loops | Permission set not assigned to agent user | Step 2 above |
leadId binding error |
leadId must be String, not Id type in Apex |
Already fixed in LeadCaptureTurnAction.cls |
USER_MODE insert fails |
Agent user lacks Lead create permission |
Included in LeadCaptureAgent_Apex_Access perm set |
| Preview session not found | --authoring-bundle flag missing on send/end |
Always pass --authoring-bundle on all three subcommands |
User message
↓
[agent_router] ← start_agent, handles greetings + routing
↓ transition
[lead_capture] ← subagent
↓ invokes
[LeadCaptureTurnAction] ← @InvocableMethod Apex
↓
- Parses userMessage for last name / company via regex
- Holds known values across turns via knownLastName / knownCompany inputs
- Inserts Lead in USER_MODE when both fields are present
- Returns isSuccess, leadId (String), resolvedLastName, resolvedCompany
↓
[Variables updated] last_name, company, lead_created, last_lead_id
- Multi-turn state: Apex receives
knownLastNameandknownCompanyas inputs per turn — the agent passes its own variables in, eliminating Flow's session limitations. - USER_MODE:
Database.insert(..., AccessLevel.USER_MODE)enforces FLS and CRUD on the agent user, avoiding over-privileged system context. leadIdasString: Agentforce binds outputs more cleanly toStringthanIdtype.
| File | Purpose |
|---|---|
force-app/main/default/aiAuthoringBundles/LeadCaptureAgent/LeadCaptureAgent.agent |
Agent Script DSL definition |
force-app/main/default/classes/LeadCaptureTurnAction.cls |
Primary invocable Apex — parses message, holds state, creates Lead |
force-app/main/default/classes/LeadCreationAction.cls |
Minimal backup Lead creator (direct insert) |
force-app/main/default/permissionsets/LeadCaptureAgent_Apex_Access.permissionset-meta.xml |
Grants agent user access to Apex classes + Lead object |
sfdx-project.json |
SFDX project config |
Pull requests welcome. See CONTRIBUTING.md.
MIT — see LICENSE.