salesforce
Skill8 starsChanged 8 months agoMITView on GitHub
Claude Code and Cowork skill for Salesforce org interaction — authenticate, query, download and upload files.
- Reads credentials
- Installs packages
6 other projects use a version of this.
Adopted by
Most recently taken up by
- Shanksg/clawskillstheir own version, first seen Nov 1, 2026
- HybridAIOne/hybridclawtheir own version, first seen Oct 13, 2026
- PranavNagrecha/AwesomeSalesforceSkillstheir own version, first seen Sep 3, 2026
- poorvith-mp/skills-developertheir own version, first seen Aug 19, 2026
- apideck-libraries/api-skillstheir own version, first seen Aug 13, 2026
- refly-ai/refly-skillstheir own version, first seen Jan 25, 2026
Other versions6 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 6
- refly-ai/refly-skills★ 204same name
- HybridAIOne/hybridclaw★ 159same name
- PranavNagrecha/AwesomeSalesforceSkills★ 17same name
- apideck-libraries/api-skills★ 4same name
- Shanksg/clawskills★ 2same name
- poorvith-mp/skills-developer★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
The file
Show its rules and text
We split this skill into the rules it gives an agent
24 rules. 0 are common, 14 are distinctive, and 10 are very specific to this project.
0
Common
Most of the 6 edited versions we read keep these.
14
Distinctive
This author's own choices. Worth a look.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 7
Use this skill when Claude Code or Cowork needs to interact with a Salesforce org: authenticate, query data, create/update/delete records, …
- Very specific: line 22
If the user sends SALESFORCE_SKIP_WARNINGS=true at any point in the conversation, treat it as if the environment variable is set: skip all …
- Distinctive: line 40
Before any write operation, display:
- Distinctive: line 56
Before any delete operation, display:
- Distinctive: line 79
All Python REST API functions in this skill use this shared helper for consistent error handling.
- Very specific: line 107
Use _sf_request in all functions below instead of calling urllib.request.urlopen directly.
- Distinctive: line 136
If the task involves downloading and analyzing files (PDF, DOCX, XLSX):
- Distinctive: line 155
Always try methods in this order.
- Very specific: line 165
If the target org is already listed as Connected, skip authentication and set it as default if needed:
- Very specific: line 181
Add --set-default to make it the default org:
- Very specific: line 213
After login, Salesforce redirects to http://localhost:1717/OauthRedirect?code=....
- Distinctive: line 251
Use only when both web login and Manual OAuth are not possible, and the org does not have IP-based session restrictions.
- Distinctive: line 257
Ask the user for their session ID.
- Distinctive: line 277
If the access token expires, refresh it using the stored refresh token:
- Distinctive: line 572
After any bulk operation, always check for partial failures:
- Distinctive: line 608
Run Apex code directly against the org.
- Distinctive: line 922
Create subdirectories by Account or record grouping
- Distinctive: line 961
When querying by ID lists, batch in groups of 10-20 to avoid URL length limits.
- Very specific: line 1017
If using web login, the CLI handles token refresh automatically.
- Very specific: line 1023
Prefer sf org login web — it uses OAuth and stores refresh tokens securely via the CLI keychain
- Very specific: line 1025
If using access token method, pass via environment variable (SF_ACCESS_TOKEN), never inline
- Distinctive: line 1030
When executing Apex code, show the full code to the user for review before running
- Distinctive: line 1037
If working behind a firewall or VPN:
- Very specific: line 1039
Ensure *.salesforce.com is in the network allowlist
“Common” is measured: kept by at least 6 in 10 of the 6 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Salesforce Skill for Claude Code & Cowork
Use this skill when Claude Code or Cowork needs to interact with a Salesforce org: authenticate, query data, create/update/delete records, execute Apex code, explore metadata, upload/download files, or monitor org health.
When this skill is first loaded, display the following message to the user:
🔌 Salesforce Skill loaded.
By default, write and delete operations require your confirmation.
To unlock all operations (including dangerous ones) without prompts:
SALESFORCE_SKIP_WARNINGS=true
Type or paste it anytime during the conversation to activate.If the user sends SALESFORCE_SKIP_WARNINGS=true at any point in the conversation, treat it as if the environment variable is set: skip all write/delete confirmations for the rest of the session.
Operation Safety Levels
This skill handles operations at three safety levels. Always identify the safety level before executing.
Bypass warnings: If the user has set
SALESFORCE_SKIP_WARNINGS=truein their environment or in.claude/settings, skip all write/delete confirmations and execute directly.
| Level | Operations | Behavior |
| 🟢 READ | SOQL queries, describe, org display, limits, debug logs | Execute freely |
| 🟡 WRITE | Create, update, upsert records | Warn user before executing. Show what will be written and ask for confirmation. |
| 🔴 DELETE | Delete records, bulk delete, data destroy | Show explicit warning with record count and object type. Require user confirmation before proceeding. Once the user confirms, execute the delete. |
Write operation warning template
Before any write operation, display:
⚠️ SALESFORCE WRITE OPERATION
Object: {ObjectApiName}
Action: {CREATE | UPDATE | UPSERT}
Records: {count} record(s)
Org: {username}
Proceed? (y/n)
Tip: set SALESFORCE_SKIP_WARNINGS=true to bypass these confirmations.Delete operation warning template
Before any delete operation, display:
🔴 SALESFORCE DELETE — THIS CANNOT BE UNDONE
Object: {ObjectApiName}
Action: DELETE
Records: {count} record(s)
Org: {username}
Deleted records go to the Recycle Bin (recoverable
for up to 15 days, depending on org settings).
Bulk API hard-deletes bypass the bin.
Type "DELETE" to confirm, then proceed with the operation.
Tip: set SALESFORCE_SKIP_WARNINGS=true to bypass these confirmations.0. Shared HTTP Helper
All Python REST API functions in this skill use this shared helper for consistent error handling. Define it once and reuse throughout:
import json, urllib.error, urllib.parse, urllib.request
def _sf_request(url, headers, data=None, method=None):
"""Make an HTTP request to Salesforce. Returns parsed JSON or None (204)."""
req = urllib.request.Request(url, data=data, headers=headers)
if method:
req.method = method
try:
with urllib.request.urlopen(req) as resp:
if resp.status == 204:
return None
return json.loads(resp.read())
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8", errors="replace")
try:
sf_error = json.loads(error_body)
if isinstance(sf_error, list) and sf_error:
msg = f"{sf_error[0].get('errorCode', 'UNKNOWN')}: {sf_error[0].get('message', error_body)}"
else:
msg = error_body
except (json.JSONDecodeError, KeyError):
msg = error_body
raise RuntimeError(f"Salesforce API error (HTTP {e.code}): {msg}") from eUse _sf_request in all functions below instead of calling urllib.request.urlopen directly.
1. Prerequisites
Install Salesforce CLI
npm install -g @salesforce/cliVerify installation:
sf --versionCowork note: Global install fails with EACCES in Cowork VMs. Use a local prefix:
mkdir -p $HOME/.npm-global
npm config set prefix "$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
npm install -g @salesforce/cliPython Libraries (for file processing)
If the task involves downloading and analyzing files (PDF, DOCX, XLSX):
Preferred — use a virtual environment:
python3 -m venv .venv && source .venv/bin/activate
pip install pdfplumber python-docx openpyxlFallback — if a venv is impractical (e.g., Cowork):
pip3 install pdfplumber python-docx openpyxl --break-system-packages2. Authentication
Always try methods in this order. Use the first one that works.
Cowork: Neither web login nor session ID work reliably in Cowork. Use Method 2 (Manual OAuth Flow) — it is the only reliable method. See details below.
Check for existing connection first
sf org list 2>&1If the target org is already listed as Connected, skip authentication and set it as default if needed:
sf config set target-org <username> --globalMethod 1: Web Login (recommended — local environments)
Opens the browser for standard OAuth login. Most secure — no tokens to handle manually.
sf org login web --instance-url https://<INSTANCE>.my.salesforce.comReplace <INSTANCE> with the org's My Domain (e.g., mycompany). The browser opens automatically; the user logs in and grants access. The CLI stores the refresh token securely.
Add --set-default to make it the default org:
sf org login web --instance-url https://<INSTANCE>.my.salesforce.com --set-defaultMethod 2: Manual OAuth Flow (Cowork and headless environments)
This is the only reliable method in Cowork. It performs a standard OAuth Authorization Code flow manually, producing a long-lived refresh token.
Step 1 — Generate the authorization URL:
import urllib.parse
INSTANCE_URL = "https://<INSTANCE>.my.salesforce.com"
CLIENT_ID = "PlatformCLI"
REDIRECT_URI = "http://localhost:1717/OauthRedirect"
auth_url = (
f"{INSTANCE_URL}/services/oauth2/authorize"
f"?response_type=code"
f"&client_id={CLIENT_ID}"
f"&redirect_uri={urllib.parse.quote(REDIRECT_URI)}"
f"&prompt=login%20consent"
f"&scope=refresh_token%20api%20web"
)
print(auth_url)Step 2 — Ask the user to open the URL in their browser and log in.
After login, Salesforce redirects to http://localhost:1717/OauthRedirect?code=.... Since no server is running on that port, the page will fail to load. Ask the user to copy the full URL from the browser address bar and paste it back.
Step 3 — Exchange the authorization code for tokens:
import urllib.parse, urllib.request, json
# Extract the code from the redirect URL the user pasted
redirect_url = "<URL_FROM_USER>"
code = urllib.parse.parse_qs(urllib.parse.urlparse(redirect_url).query)["code"][0]
INSTANCE_URL = "https://<INSTANCE>.my.salesforce.com"
CLIENT_ID = "PlatformCLI"
REDIRECT_URI = "http://localhost:1717/OauthRedirect"
data = urllib.parse.urlencode({
"grant_type": "authorization_code",
"code": code,
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI
}).encode()
req = urllib.request.Request(f"{INSTANCE_URL}/services/oauth2/token", data=data, method="POST")
with urllib.request.urlopen(req) as resp:
token_data = json.loads(resp.read())
# token_data contains: access_token, refresh_token, instance_url, scope, etc.
instance_url = token_data["instance_url"]
access_token = token_data["access_token"]
refresh_token = token_data["refresh_token"]Step 4 — Use REST API directly (bypass sf CLI in Cowork).
The sf CLI has a DNS resolution bug in Cowork VMs (DomainNotFoundError). Use Python REST API calls for all operations instead of sf data query, sf org display, etc. See the Python examples throughout this skill.
Method 3: Access Token (last resort)
Use only when both web login and Manual OAuth are not possible, and the org does not have IP-based session restrictions.
Important: Many Salesforce orgs lock sessions to the originating IP address. If the session ID was obtained from a different IP (e.g., user's browser vs. Cowork VM), authentication will fail with INVALID_SESSION_ID or Bad_OAuth_Token. In that case, use Method 2.
Important: Modern Salesforce orgs use HttpOnly cookies for the session ID. The document.cookie trick does not work in those orgs.
Ask the user for their session ID. They can get it from one of these methods:
- Developer Console — Open Developer Console, execute anonymous Apex:
System.debug(UserInfo.getSessionId());, copy from the debug log - URL in Classic UI — Switch to Classic, copy the
sid=parameter from the URL - Browser cookie (only if not HttpOnly) — F12 → Application → Cookies → copy
sidvalue
Then authenticate:
export SF_ACCESS_TOKEN="<TOKEN_FROM_USER>"
sf org login access-token \
--instance-url https://<INSTANCE>.my.salesforce.com \
--no-prompt \
--set-defaultNote: Session IDs expire after 2-12 hours depending on org settings. Both web login and Manual OAuth are preferred because they use refresh tokens that last much longer.
Token Refresh
If the access token expires, refresh it using the stored refresh token:
def sf_refresh_token(refresh_token, instance_url):
"""Refresh an expired access token. Returns (new_access_token, instance_url).
Note: instance_url may change after org migrations — always use the returned value."""
data = urllib.parse.urlencode({
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": "PlatformCLI"
}).encode()
headers = {"Content-Type": "application/x-www-form-urlencoded"}
result = _sf_request(
f"{instance_url}/services/oauth2/token", headers=headers, data=data
)
return result["access_token"], result.get("instance_url", instance_url)Refresh tokens last for months (vs hours for session IDs).
Verify connection
sf org display --jsonOr via REST API (recommended in Cowork):
def sf_verify_connection(instance_url, access_token):
"""Verify the connection by fetching org limits."""
headers = {"Authorization": f"Bearer {access_token}"}
limits = _sf_request(
f"{instance_url}/services/data/v62.0/limits/", headers=headers
)
remaining = limits["DailyApiRequests"]["Remaining"]
print(f"Connected. API calls remaining today: {remaining}")
return limitsAPI version note: This skill uses API v62.0 (Spring '26). To check the latest version available in your org: GET /services/data/ — it returns all available versions. Replace v62.0 throughout if needed.
3. Running SOQL Queries 🟢
Via sf CLI
sf data query --query "SELECT Id, Name FROM Account LIMIT 10" --jsonBulk queries
For large result sets or queries that may time out, add --bulk to use Bulk API 2.0:
sf data query --query "SELECT Id, Name FROM Account" --bulk --wait 10 --jsonNote: The standard REST query endpoint paginates and handles any result size. Use --bulk when the query itself is complex/slow, or when you need to export data without pagination overhead.
Tooling API queries
Query metadata objects using the Tooling API:
sf data query --query "SELECT Id, Name, Status FROM ApexClass WHERE Status = 'Active'" \
--use-tooling-api --jsonVia REST API (Python)
For programmatic access with pagination support. Required in Cowork (sf CLI has DNS issues).
def sf_query(soql, instance_url, access_token):
"""Execute a SOQL query with automatic pagination."""
api_base = f"{instance_url}/services/data/v62.0"
url = f"{api_base}/query/?q={urllib.parse.quote(soql)}"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
data = _sf_request(url, headers=headers)
records = data.get("records", [])
# Handle pagination
while data.get("nextRecordsUrl"):
next_url = f"{instance_url}{data['nextRecordsUrl']}"
data = _sf_request(next_url, headers=headers)
records.extend(data.get("records", []))
return recordsNote: The Tooling API also paginates via nextRecordsUrl but uses a different base path (/services/data/v62.0/tooling/query/). Use the same pagination pattern.
Getting credentials from sf CLI (Python)
import json, subprocess
def get_sf_credentials():
"""Get instanceUrl and accessToken from the authenticated sf CLI org."""
result = subprocess.run(
["sf", "org", "display", "--json"],
capture_output=True, text=True
)
if result.returncode != 0:
raise RuntimeError(f"sf org display failed: {result.stderr.strip()}")
parsed = json.loads(result.stdout)
if parsed.get("status") != 0:
raise RuntimeError(f"sf org display error: {parsed.get('message', result.stderr)}")
data = parsed["result"]
return data["instanceUrl"].rstrip("/"), data["accessToken"]4. Discovering Objects and Fields 🟢
List all custom objects
sf data query --query \
"SELECT QualifiedApiName, Label FROM EntityDefinition WHERE QualifiedApiName LIKE '%__c'" \
--jsonNote: EntityDefinition does NOT support OR/disjunctions in WHERE. Query each condition separately.
Describe an object's fields
Via sf CLI:
sf sobject describe --sobject <ObjectApiName> --jsonVia REST API (required in Cowork):
def sf_describe(sobject, instance_url, access_token):
"""Describe an object's fields via REST API."""
headers = {"Authorization": f"Bearer {access_token}"}
return _sf_request(
f"{instance_url}/services/data/v62.0/sobjects/{sobject}/describe/",
headers=headers
)Parse the fields array. Each field has: name, type, label.
List all objects (standard + custom)
sf sobject list --jsonCommon field filters for describe output
# Filter fields by relevance
for f in fields:
if any(kw in f['name'].lower() for kw in ['date', 'amount', 'status', 'name', 'account']):
print(f"{f['name']} ({f['type']}) - {f['label']}")Record types for an object
SELECT Id, Name, DeveloperName, IsActive
FROM RecordType
WHERE SObjectType = '<ObjectApiName>'
AND IsActive = true5. CRUD Operations
5a. Create Record 🟡
WRITE OPERATION — confirm with user before executing.
sf data create record --sobject Account \
--values "Name='Acme Corp' Industry='Technology' Website='https://acme.example.com'" \
--jsonVia REST API (Python):
def sf_create_record(sobject, record_data, instance_url, access_token):
"""Create a single record. Returns the new record ID."""
url = f"{instance_url}/services/data/v62.0/sobjects/{sobject}/"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
data = json.dumps(record_data).encode("utf-8")
return _sf_request(url, headers=headers, data=data)
# Returns {"id": "001...", "success": true}5b. Update Record 🟡
WRITE OPERATION — confirm with user before executing.
sf data update record --sobject Account \
--record-id 001XXXXXXXXXXXX \
--values "Industry='Finance' Rating='Hot'" \
--jsonVia REST API (Python):
def sf_update_record(sobject, record_id, update_data, instance_url, access_token):
"""Update fields on an existing record."""
url = f"{instance_url}/services/data/v62.0/sobjects/{sobject}/{record_id}"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
data = json.dumps(update_data).encode("utf-8")
_sf_request(url, headers=headers, data=data, method="PATCH")
# Returns None (204 No Content) on success5c. Delete Record 🔴
DELETE OPERATION — show warning and ask for confirmation. Once confirmed, execute.
sf data delete record --sobject Account \
--record-id 001XXXXXXXXXXXX \
--jsonVia REST API (Python):
def sf_delete_record(sobject, record_id, instance_url, access_token):
"""Delete a single record. Returns None (204) on success."""
url = f"{instance_url}/services/data/v62.0/sobjects/{sobject}/{record_id}"
headers = {"Authorization": f"Bearer {access_token}"}
_sf_request(url, headers=headers, method="DELETE")5d. Upsert (Insert or Update) 🟡
WRITE OPERATION — confirm with user before executing.
Upsert uses an external ID field to decide whether to insert or update:
sf data upsert record --sobject Contact \
--external-id Email \
--values "Email='jdoe@example.com' FirstName='John' LastName='Doe'" \
--json6. Bulk Operations
Bulk Upsert from CSV 🟡
BULK WRITE — this affects many records. Show record count from CSV before executing.
sf data upsert bulk --sobject Contact \
--file contacts.csv \
--external-id Id \
--wait 10 \
--jsonBulk Delete from CSV 🔴
BULK DELETE — show CSV row count and object name. Ask for confirmation, then execute.
sf data delete bulk --sobject Contact \
--file contacts_to_delete.csv \
--wait 10 \
--jsonChecking bulk job results
After any bulk operation, always check for partial failures:
sf data bulk results --job-id <JOB_ID> --jsonThe result includes numberRecordsFailed. If > 0, retrieve the failed-records CSV to inspect individual errors:
GET /services/data/v62.0/jobs/ingest/<JOB_ID>/failedResults/
Authorization: Bearer <ACCESS_TOKEN>
Accept: text/csvData Export (Tree format) 🟢
Export records preserving relationships:
sf data export tree --query "SELECT Id, Name, (SELECT Id, LastName FROM Contacts) FROM Account WHERE Industry = 'Technology'" \
--output-dir ./export/ \
--jsonData Import (Tree format) 🟡
WRITE OPERATION — confirm with user. Show file contents summary.
sf data import tree --files ./export/Account.json --json7. Execute Anonymous Apex 🟡
Run Apex code directly against the org. Always show the code to the user and confirm before executing. Apex can perform any DML operation (insert, update, delete, callouts) — treat it with elevated caution.
Inline execution
sf apex run --file script.apex --jsonFrom stdin
echo "System.debug('Hello from Claude Code');" | sf apex run --jsonExample: Mass update via Apex 🟡
// Update all Contacts missing a MailingCountry
List<Contact> contacts = [
SELECT Id, MailingCountry
FROM Contact
WHERE MailingCountry = null
LIMIT 200
];
for (Contact c : contacts) {
c.MailingCountry = 'US';
}
update contacts;
System.debug('Updated ' + contacts.size() + ' contacts');Apex can perform any DML operation. Always review the code with the user before execution.
Execute Apex via REST API (Cowork fallback)
def sf_execute_apex(apex_code, instance_url, access_token):
"""Execute anonymous Apex via REST API (POST to avoid URL length limits)."""
url = f"{instance_url}/services/data/v62.0/tooling/executeAnonymous/"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/x-www-form-urlencoded",
}
data = urllib.parse.urlencode({"anonymousBody": apex_code}).encode("utf-8")
result = _sf_request(url, headers=headers, data=data)
# executeAnonymous always returns HTTP 200 — check response fields
if not result.get("compiled"):
raise RuntimeError(
f"Apex compile error at line {result.get('line')}: "
f"{result.get('compileProblem')}"
)
if not result.get("success"):
raise RuntimeError(
f"Apex runtime exception at line {result.get('line')}: "
f"{result.get('exceptionMessage')}"
)
return result8. Debug Logs & Monitoring 🟢
View recent debug logs
sf apex list log --jsonGet a specific log
sf apex get log --log-id <LOG_ID> --jsonTail logs in real-time
sf apex tail log --colorCheck org API limits
sf org display --jsonVia REST API:
GET /services/data/v62.0/limits/
Authorization: Bearer <ACCESS_TOKEN>Useful limits to monitor:
DailyApiRequests— total API calls remainingDailyBulkV2QueryJobs— bulk query jobsSingleEmail— email sends remaining
9. Metadata Operations 🟢
List metadata components
sf org list metadata-types --jsonRetrieve metadata
sf project retrieve start --metadata "ApexClass:MyClassName" --jsonRetrieve by manifest
sf project retrieve start --manifest manifest/package.xml --jsonDeploy metadata 🟡
DEPLOY modifies org configuration. Confirm target org and components with user.
sf project deploy start --metadata "ApexClass:MyClassName" --jsonDry-run validation (no changes applied):
sf project deploy start --metadata "ApexClass:MyClassName" --dry-run --json10. Files: Download and Upload (ContentVersion) 🟢/🟡
Salesforce stores files via ContentDocument / ContentVersion / ContentDocumentLink.
Step 1: Find files linked to a record
SELECT ContentDocumentId, ContentDocument.Title,
ContentDocument.FileType, ContentDocument.FileExtension
FROM ContentDocumentLink
WHERE LinkedEntityId = '<RECORD_ID>'Batch query (up to 200 IDs per IN clause, but 10-20 recommended for URL length):
SELECT ContentDocumentId, LinkedEntityId,
ContentDocument.Title, ContentDocument.FileExtension
FROM ContentDocumentLink
WHERE LinkedEntityId IN ('id1','id2','id3')Step 2: Get the latest version
SELECT Id, Title, FileExtension
FROM ContentVersion
WHERE ContentDocumentId = '<DOC_ID>'
AND IsLatest = trueStep 3: Download the binary 🟢
GET /services/data/v62.0/sobjects/ContentVersion/<VERSION_ID>/VersionData
Authorization: Bearer <ACCESS_TOKEN>Python implementation (streams to disk to handle large files):
def download_sf_file(version_id, filepath, instance_url, access_token):
"""Download a file from Salesforce ContentVersion (streamed)."""
url = f"{instance_url}/services/data/v62.0/sobjects/ContentVersion/{version_id}/VersionData"
req = urllib.request.Request(url, headers={
"Authorization": f"Bearer {access_token}"
})
try:
with urllib.request.urlopen(req) as resp:
# Check for HTML error pages (e.g., expired token returns login page)
content_type = resp.headers.get("Content-Type", "")
if "text/html" in content_type:
raise RuntimeError(
"Salesforce returned HTML instead of file data — "
"token may be expired or permissions insufficient"
)
with open(filepath, "wb") as f:
while chunk := resp.read(65536):
f.write(chunk)
except urllib.error.HTTPError as e:
raise RuntimeError(f"File download failed (HTTP {e.code}): {e.read().decode('utf-8', errors='replace')}") from e
return filepathStep 4: Upload a file 🟡
WRITE OPERATION — confirm with user before uploading. Show filename, size, and target record.
To upload a file to Salesforce, create a ContentVersion record. Salesforce automatically creates the parent ContentDocument.
import base64, os
def upload_sf_file(filepath, instance_url, access_token, linked_entity_id=None, title=None):
"""Upload a local file to Salesforce as a ContentVersion.
Optionally link it to a record (Account, Case, etc.) via ContentDocumentLink.
Returns the new ContentVersion record."""
filename = os.path.basename(filepath)
if title is None:
title = os.path.splitext(filename)[0]
with open(filepath, "rb") as f:
file_data = base64.b64encode(f.read()).decode("ascii")
# Create ContentVersion with base64-encoded body
cv_data = {
"Title": title,
"PathOnClient": filename,
"VersionData": file_data
}
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
result = _sf_request(
f"{instance_url}/services/data/v62.0/sobjects/ContentVersion/",
headers=headers,
data=json.dumps(cv_data).encode("utf-8")
)
cv_id = result["id"]
# If a linked_entity_id is provided, link the file to that record
if linked_entity_id:
# Get the ContentDocumentId from the newly created ContentVersion
cv_record = _sf_request(
f"{instance_url}/services/data/v62.0/sobjects/ContentVersion/{cv_id}",
headers=headers
)
doc_id = cv_record["ContentDocumentId"]
# Create ContentDocumentLink
link_data = {
"ContentDocumentId": doc_id,
"LinkedEntityId": linked_entity_id,
"ShareType": "V", # Viewer
"Visibility": "AllUsers"
}
_sf_request(
f"{instance_url}/services/data/v62.0/sobjects/ContentDocumentLink/",
headers=headers,
data=json.dumps(link_data).encode("utf-8")
)
return resultUpload via multipart (large files): For files > 37.5 MB (base64 overhead on the 50 MB REST API limit), use multipart form upload:
import uuid
def upload_sf_file_multipart(filepath, instance_url, access_token, title=None):
"""Upload a large file using multipart/form-data (up to 2 GB via REST)."""
filename = os.path.basename(filepath)
if title is None:
title = os.path.splitext(filename)[0]
boundary = uuid.uuid4().hex
# Build multipart body
metadata = json.dumps({
"Title": title,
"PathOnClient": filename
})
with open(filepath, "rb") as f:
file_content = f.read()
body = (
f"--{boundary}\r\n"
f"Content-Disposition: form-data; name=\"entity_content\"\r\n"
f"Content-Type: application/json\r\n\r\n"
f"{metadata}\r\n"
f"--{boundary}\r\n"
f"Content-Disposition: form-data; name=\"VersionData\"; filename=\"{filename}\"\r\n"
f"Content-Type: application/octet-stream\r\n\r\n"
).encode("utf-8") + file_content + f"\r\n--{boundary}--\r\n".encode("utf-8")
req = urllib.request.Request(
f"{instance_url}/services/data/v62.0/sobjects/ContentVersion/",
data=body,
method="POST",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": f"multipart/form-data; boundary={boundary}"
}
)
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8", errors="replace")
raise RuntimeError(f"Upload failed (HTTP {e.code}): {error_body}") from eOrganizing downloaded files
- Create subdirectories by Account or record grouping
- Sanitize filenames:
re.sub(r'[<>:"/\\|?*]', '_', name) - Skip already-downloaded files to allow resumable downloads
- Log file sizes for verification
11. Common Patterns
Query Opportunities with Account info
SELECT Id, Name, Account.Name, CloseDate, Amount, StageName
FROM Opportunity
WHERE StageName = 'Closed Won'
AND CloseDate >= 2024-01-01
ORDER BY CloseDate DESCQuery Contacts with Account relationship
SELECT Id, FirstName, LastName, Email, Account.Name
FROM Contact
WHERE Account.Industry = 'Technology'
ORDER BY LastName ASCQuery Cases with related Contact
SELECT Id, CaseNumber, Subject, Status, Contact.Name, Contact.Email
FROM Case
WHERE Status != 'Closed'
ORDER BY CreatedDate DESCBatch processing pattern
When querying by ID lists, batch in groups of 10-20 to avoid URL length limits.
Note: This pattern is safe for system-generated Salesforce IDs (15/18-char alphanumeric). Do NOT use it for user-supplied string values — that would create a SOQL injection risk.
for batch_start in range(0, len(record_ids), 10):
batch = record_ids[batch_start:batch_start + 10]
ids_str = "','".join(batch)
query = f"SELECT ... FROM ... WHERE Id IN ('{ids_str}')"
records = sf_query(query, instance_url, token)Composite requests (multiple operations in one call) 🟡
def sf_composite(requests, instance_url, access_token, all_or_none=False):
"""Execute multiple API operations in a single call (max 25).
all_or_none=True: rolls back all if any fails (transactional).
all_or_none=False (default): partial success possible."""
url = f"{instance_url}/services/data/v62.0/composite"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
body = json.dumps({
"allOrNone": all_or_none,
"compositeRequest": requests
}).encode("utf-8")
return _sf_request(url, headers=headers, data=body)12. Troubleshooting
| Issue | Solution |
NOT_FOUND on sobject describe | Object API name is wrong. Query EntityDefinition to find the correct QualifiedApiName |
MALFORMED_QUERY with OR | EntityDefinition doesn't support disjunctions. Use separate queries |
INVALID_SESSION_ID | Token expired or IP-locked. Re-run sf org login web (preferred), use Manual OAuth (Method 2), or ask user for a new session ID |
Bad_OAuth_Token | Session ID from a different IP. Use Manual OAuth flow (Method 2) instead |
REQUEST_LIMIT_EXCEEDED | Too many API calls. Add delays between batches or reduce batch size |
| URL too long | Reduce batch size for IN clauses (max 10-20 IDs per query) |
| Empty file download | Check that IsLatest = true filter is applied on ContentVersion |
| HTML instead of file data | Token expired or insufficient permissions — Salesforce returns a login page. Check Content-Type header before writing to disk |
pip install fails on macOS | Use a virtual environment first; fall back to --break-system-packages |
npm install -g fails with EACCES | Use local prefix: npm config set prefix "$HOME/.npm-global" |
CannotOpenBrowserError | No browser available (Cowork/CI). Use Manual OAuth flow (Method 2) |
DomainNotFoundError in sf CLI | DNS resolution bug in Cowork VMs. Use Python REST API calls instead of sf CLI |
ENTITY_IS_DELETED | Record is in Recycle Bin. Use queryAll to find deleted records |
FIELD_CUSTOM_VALIDATION_EXCEPTION | Validation rule blocking the operation. Check field values against org rules |
UNABLE_TO_LOCK_ROW | Record lock contention. Retry after a brief delay |
| Bulk job partial failure | Check numberRecordsFailed in job result. Retrieve failed-records CSV via /jobs/ingest/<JOB_ID>/failedResults/ |
Session expiration
If using web login, the CLI handles token refresh automatically. If using Manual OAuth (Method 2), use the sf_refresh_token() function to renew the access token — refresh tokens last for months. If using access token (Method 3, session ID), it expires after 2-12 hours depending on org settings.
13. Security Notes
- Prefer
sf org login web— it uses OAuth and stores refresh tokens securely via the CLI keychain - Never store access tokens in files, scripts, or commit history
- If using access token method, pass via environment variable (
SF_ACCESS_TOKEN), never inline - Downloaded files may contain sensitive business data — handle accordingly
- Clean up temporary files after processing
- Always confirm write operations with the user before execution (unless
SALESFORCE_SKIP_WARNINGS=true) - Delete operations require explicit user confirmation — but once the user confirms, proceed with the deletion (unless
SALESFORCE_SKIP_WARNINGS=true, which skips the confirmation step entirely) - When executing Apex code, show the full code to the user for review before running
- Be cautious with bulk operations — verify record counts and target object before proceeding
14. Network Requirements
If working behind a firewall or VPN:
- Ensure
*.salesforce.comis in the network allowlist - For Claude Code with Cowork: add
*.salesforce.comto Settings → Capabilities → Domain allowlist - A new Cowork session may be required after modifying the allowlist
- In Cowork, the sf CLI may fail with
DomainNotFoundErroreven with the domain allowlisted — use Python REST API calls as fallback
15. Quick Reference Card
Salesƒorce CLI — Quick Commands
─────────────────────────────────────────
🟢 READ
sf org list # list connected orgs
sf org display --json # show org details + token
sf data query --query "..." --json # SOQL query
sf data query --query "..." --bulk # bulk query
sf sobject describe --sobject X # describe object fields
sf sobject list --json # list all objects
sf apex list log --json # list debug logs
sf apex tail log --color # tail logs live
sf org list metadata-types --json # list metadata types
─────────────────────────────────────────
🟡 WRITE (confirm with user first)
sf data create record --sobject X # create record
sf data update record --sobject X # update record
sf data upsert bulk --sobject X # bulk upsert from CSV
sf data import tree --files X.json # import related records
sf apex run --file script.apex # execute Apex (show code!)
sf project deploy start # deploy metadata
─────────────────────────────────────────
🔴 DELETE (require explicit confirmation)
sf data delete record --sobject X # delete single record
sf data delete bulk --sobject X # bulk delete from CSV
─────────────────────────────────────────Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

