# Salesforce

> Claude Code and Cowork skill for Salesforce org interaction — authenticate, query, download and upload files.

- **Type:** Skill
- **Install:** `agentstack add skill-enzoleonardi-claude-salesforce-skill-salesforce`
- **Verified:** Pending review
- **Seller:** [enzoleonardi](https://agentstack.voostack.com/s/enzoleonardi)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [enzoleonardi](https://github.com/enzoleonardi)
- **Source:** https://github.com/enzoleonardi/claude-salesforce-skill/tree/main/skills/salesforce
- **Website:** https://enzoleonardi.it

## Install

```sh
agentstack add skill-enzoleonardi-claude-salesforce-skill-salesforce
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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=true` in 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:

```python
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 e
```

Use `_sf_request` in all functions below instead of calling `urllib.request.urlopen` directly.

---

## 1. Prerequisites

### Install Salesforce CLI

```bash
npm install -g @salesforce/cli
```

Verify installation:

```bash
sf --version
```

**Cowork note:** Global install fails with `EACCES` in Cowork VMs. Use a local prefix:

```bash
mkdir -p $HOME/.npm-global
npm config set prefix "$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
npm install -g @salesforce/cli
```

### Python Libraries (for file processing)

If the task involves downloading and analyzing files (PDF, DOCX, XLSX):

Preferred — use a virtual environment:

```bash
python3 -m venv .venv && source .venv/bin/activate
pip install pdfplumber python-docx openpyxl
```

Fallback — if a venv is impractical (e.g., Cowork):

```bash
pip3 install pdfplumber python-docx openpyxl --break-system-packages
```

---

## 2. 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

```bash
sf org list 2>&1
```

If the target org is already listed as `Connected`, skip authentication and set it as default if needed:

```bash
sf config set target-org  --global
```

### Method 1: Web Login (recommended — local environments)

Opens the browser for standard OAuth login. Most secure — no tokens to handle manually.

```bash
sf org login web --instance-url https://.my.salesforce.com
```

Replace `` 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:

```bash
sf org login web --instance-url https://.my.salesforce.com --set-default
```

### Method 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:**

```python
import urllib.parse

INSTANCE_URL = "https://.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:**

```python
import urllib.parse, urllib.request, json

# Extract the code from the redirect URL the user pasted
redirect_url = ""
code = urllib.parse.parse_qs(urllib.parse.urlparse(redirect_url).query)["code"][0]

INSTANCE_URL = "https://.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:

1. **Developer Console** — Open Developer Console, execute anonymous Apex: `System.debug(UserInfo.getSessionId());`, copy from the debug log
2. **URL in Classic UI** — Switch to Classic, copy the `sid=` parameter from the URL
3. **Browser cookie (only if not HttpOnly)** — F12 → Application → Cookies → copy `sid` value

Then authenticate:

```bash
export SF_ACCESS_TOKEN=""
sf org login access-token \
  --instance-url https://.my.salesforce.com \
  --no-prompt \
  --set-default
```

**Note:** 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:

```python
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

```bash
sf org display --json
```

Or via REST API (recommended in Cowork):

```python
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 limits
```

**API 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

```bash
sf data query --query "SELECT Id, Name FROM Account LIMIT 10" --json
```

### Bulk queries

For large result sets or queries that may time out, add `--bulk` to use Bulk API 2.0:

```bash
sf data query --query "SELECT Id, Name FROM Account" --bulk --wait 10 --json
```

**Note:** 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:

```bash
sf data query --query "SELECT Id, Name, Status FROM ApexClass WHERE Status = 'Active'" \
  --use-tooling-api --json
```

### Via REST API (Python)

For programmatic access with pagination support. **Required in Cowork** (sf CLI has DNS issues).

```python
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 records
```

**Note:** 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)

```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

```bash
sf data query --query \
  "SELECT QualifiedApiName, Label FROM EntityDefinition WHERE QualifiedApiName LIKE '%__c'" \
  --json
```

**Note:** `EntityDefinition` does NOT support OR/disjunctions in WHERE. Query each condition separately.

### Describe an object's fields

Via sf CLI:

```bash
sf sobject describe --sobject  --json
```

Via REST API (required in Cowork):

```python
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)

```bash
sf sobject list --json
```

### Common field filters for describe output

```python
# 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

```sql
SELECT Id, Name, DeveloperName, IsActive
FROM RecordType
WHERE SObjectType = ''
AND IsActive = true
```

---

## 5. CRUD Operations

### 5a. Create Record 🟡

**WRITE OPERATION — confirm with user before executing.**

```bash
sf data create record --sobject Account \
  --values "Name='Acme Corp' Industry='Technology' Website='https://acme.example.com'" \
  --json
```

Via REST API (Python):

```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.**

```bash
sf data update record --sobject Account \
  --record-id 001XXXXXXXXXXXX \
  --values "Industry='Finance' Rating='Hot'" \
  --json
```

Via REST API (Python):

```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 success
```

### 5c. Delete Record 🔴

**DELETE OPERATION — show warning and ask for confirmation. Once confirmed, execute.**

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [enzoleonardi](https://github.com/enzoleonardi)
- **Source:** [enzoleonardi/claude-salesforce-skill](https://github.com/enzoleonardi/claude-salesforce-skill)
- **License:** MIT
- **Homepage:** https://enzoleonardi.it

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** yes
- **Shell / process execution:** yes
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-enzoleonardi-claude-salesforce-skill-salesforce
- Seller: https://agentstack.voostack.com/s/enzoleonardi
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
