sf-exit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ .DS_Store
14
+ .idea/
15
+ .vscode/
16
+
17
+ # Exports contain customer data: never commit them by accident
18
+ sf-export/
19
+ export/
20
+ *.duckdb
21
+ *.db
22
+ *.parquet
23
+ server.key
24
+ .env
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.1.0] - 2026-11-14
8
+
9
+ First public release.
10
+
11
+ ### Added
12
+ - `sf-exit export`: every queryable object to CSV, via Bulk API 2.0 (large objects) or REST (small objects), with automatic fallback between them.
13
+ - Downloads of ContentVersion, Attachment and Document bodies, verified against Salesforce's MD5 checksums, plus `files/index.csv`.
14
+ - `ContentDocumentLink` export (file ↔ record links), which Salesforce won't return without a filter.
15
+ - Resumable exports (`.sf-exit/state.json`), `--include-deleted`, include/exclude globs.
16
+ - Schema output: per-object describe JSON, DDL for Postgres/DuckDB/SQLite, a Mermaid ER diagram, `manifest.json` and `REPORT.md`.
17
+ - `sf-exit load` into SQLite, DuckDB, Postgres and Parquet, with typed columns.
18
+ - `sf-exit verify` (offline row counts and SHA-256, and `--live` comparison against the org).
19
+ - `sf-exit plan` (dry run with API-limit estimate) and `sf-exit doctor` (permission check).
20
+ - Login via Salesforce CLI, access token, OAuth client credentials, JWT bearer or username-password.
21
+ - GitHub Action and Docker image.
22
+ - Composite batch requests: describes and small-object queries are fetched 25 per API call.
23
+ - Knowledge articles (`KnowledgeArticleVersion`, `*__kav`) are exported per `PublishStatus`.
24
+ - Objects that Salesforce refuses to export unfiltered are reported as skipped (with the reason) instead of failing the run. Permission and limit errors still count as failures.
25
+ - Resuming with different options (for example `--include-deleted`) starts a clean export instead of mixing results.
26
+ - Bulk jobs that don't finish within 6 hours are aborted and reported, instead of being polled forever.
27
+
28
+ [0.1.0]: https://github.com/akhsterminator/sf-exit/releases/tag/v0.1.0
sf_exit-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 akhsterminator and sf-exit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
sf_exit-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,327 @@
1
+ Metadata-Version: 2.5
2
+ Name: sf-exit
3
+ Version: 0.1.0
4
+ Summary: Leave Salesforce with all your data. One command exports every object, file and attachment to CSV, then loads it into SQLite, DuckDB, Postgres or Parquet.
5
+ Project-URL: Homepage, https://github.com/akhsterminator/sf-exit
6
+ Project-URL: Documentation, https://github.com/akhsterminator/sf-exit#readme
7
+ Project-URL: Issues, https://github.com/akhsterminator/sf-exit/issues
8
+ Project-URL: Changelog, https://github.com/akhsterminator/sf-exit/blob/main/CHANGELOG.md
9
+ Author: sf-exit contributors
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: backup,bulk-api,crm,data-migration,data-portability,duckdb,etl,export,migration,parquet,postgres,salesforce,sqlite
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Information Technology
17
+ Classifier: Intended Audience :: System Administrators
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
26
+ Classifier: Topic :: Database
27
+ Classifier: Topic :: System :: Archiving :: Backup
28
+ Classifier: Topic :: Utilities
29
+ Requires-Python: >=3.10
30
+ Requires-Dist: click>=8.1
31
+ Requires-Dist: requests>=2.28
32
+ Requires-Dist: rich>=13.0
33
+ Provides-Extra: all
34
+ Requires-Dist: duckdb>=1.1; extra == 'all'
35
+ Requires-Dist: psycopg[binary]>=3.1; extra == 'all'
36
+ Requires-Dist: pyjwt[crypto]>=2.8; extra == 'all'
37
+ Provides-Extra: dev
38
+ Requires-Dist: duckdb>=1.1; extra == 'dev'
39
+ Requires-Dist: pytest>=7; extra == 'dev'
40
+ Requires-Dist: ruff>=0.5; extra == 'dev'
41
+ Provides-Extra: duckdb
42
+ Requires-Dist: duckdb>=1.1; extra == 'duckdb'
43
+ Provides-Extra: jwt
44
+ Requires-Dist: pyjwt[crypto]>=2.8; extra == 'jwt'
45
+ Provides-Extra: parquet
46
+ Requires-Dist: duckdb>=1.1; extra == 'parquet'
47
+ Provides-Extra: postgres
48
+ Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
49
+ Description-Content-Type: text/markdown
50
+
51
+ <div align="center">
52
+
53
+ # sf-exit
54
+
55
+ **Leave Salesforce with all of your data.**
56
+
57
+ One command exports every record, file and attachment from a Salesforce org,<br>
58
+ then loads it into **SQLite, DuckDB, Postgres or Parquet**, with every Id and relationship kept.
59
+
60
+ [![CI](https://github.com/akhsterminator/sf-exit/actions/workflows/ci.yml/badge.svg)](https://github.com/akhsterminator/sf-exit/actions/workflows/ci.yml)
61
+ [![PyPI](https://img.shields.io/pypi/v/sf-exit?color=blue)](https://pypi.org/project/sf-exit/)
62
+ [![Python](https://img.shields.io/pypi/pyversions/sf-exit)](https://pypi.org/project/sf-exit/)
63
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
64
+ [![GitHub stars](https://img.shields.io/github/stars/akhsterminator/sf-exit?style=social)](https://github.com/akhsterminator/sf-exit/stargazers)
65
+
66
+ [Quick start](#quick-start) · [What you get](#what-you-get) · [Why](#why-sf-exit) · [Nightly backups](#nightly-backups-with-github-actions) · [FAQ](#faq) · [Roadmap](#roadmap)
67
+
68
+ <img src="docs/demo.svg" alt="sf-exit exporting a Salesforce org and loading it into DuckDB" width="820">
69
+
70
+ <sub>A real run, recorded against the simulated org that the test suite uses (<code>scripts/record_demo.py</code>).</sub>
71
+
72
+ </div>
73
+
74
+ ---
75
+
76
+ ## Quick start
77
+
78
+ ```bash
79
+ pipx install sf-exit # or: pip install sf-exit / uvx sf-exit
80
+ sf org login web --alias my-org # log in once with the Salesforce CLI
81
+ sf-exit export --org my-org -d ./export # every object + every file
82
+ sf-exit load ./export --to duckdb:///salesforce.duckdb
83
+ ```
84
+
85
+ That's it. There's no connected app to configure and no Salesforce setup to change: sf-exit reuses your [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli) login. Other login methods (access token, JWT, OAuth client credentials, username + password) are covered [below](#authentication).
86
+
87
+ Not sure what you're in for? Run a **dry run** first:
88
+
89
+ ```bash
90
+ sf-exit plan --org my-org # objects, row counts, file storage, API calls needed vs. your limits
91
+ sf-exit doctor --org my-org # checks you have "View All Data" and "Query All Files"
92
+ ```
93
+
94
+ ## What you get
95
+
96
+ ```text
97
+ export/
98
+ ├── data/
99
+ │ ├── Account.csv ← one CSV per object, every field, original 18-char Ids
100
+ │ ├── Contact.csv
101
+ │ ├── Invoice__c.csv ← custom objects too
102
+ │ ├── ContentDocumentLink.csv ← which file belongs to which record
103
+ │ └── … (every queryable object: history, tasks, emails, chatter, custom metadata…)
104
+ ├── files/
105
+ │ ├── ContentVersion/<ContentDocumentId>/v1_Contract.pdf
106
+ │ ├── Attachment/<ParentId>/<Id>_logo.png
107
+ │ ├── Document/<FolderId>/<Id>_Letterhead.docx
108
+ │ └── index.csv ← file → record mapping, SHA-256, MD5 checked against Salesforce
109
+ ├── schema/Account.json ← full describe: labels, picklist values, relationships
110
+ ├── schema.postgres.sql ← ready-to-run DDL (also .duckdb.sql / .sqlite.sql)
111
+ ├── erd.mmd ← Mermaid diagram of your data model
112
+ ├── manifest.json ← counts + checksums, machine-readable
113
+ └── REPORT.md ← human summary: what was exported, what was skipped and why
114
+ ```
115
+
116
+ Then query it however you like:
117
+
118
+ ```sql
119
+ -- duckdb salesforce.duckdb
120
+ SELECT a.Name, count(*) AS open_opps, sum(o.Amount) AS pipeline
121
+ FROM Opportunity o JOIN Account a ON a.Id = o.AccountId
122
+ WHERE NOT o.IsClosed
123
+ GROUP BY 1 ORDER BY pipeline DESC LIMIT 10;
124
+ ```
125
+
126
+ ## Why sf-exit
127
+
128
+ Your CRM data belongs to you, but getting *all* of it out of Salesforce in a usable form is surprisingly hard.
129
+
130
+ | | **sf-exit** | Data Export Service | Data Loader | Paid backup tools |
131
+ |---------------------------------------------|:---:|:---:|:---:|:---:|
132
+ | Every object in one run | ✅ | ✅ | ❌ one at a time | ✅ |
133
+ | Files, attachments & documents, linked to records | ✅ | ✅ zipped | ❌ | ✅ |
134
+ | Run whenever you want | ✅ | ❌ weekly or monthly | ✅ | ✅ |
135
+ | Resumes after interruption | ✅ | ❌ | ❌ | varies |
136
+ | Typed SQL schema + relationships | ✅ | ❌ | ❌ | rarely |
137
+ | Loads into Postgres / DuckDB / SQLite / Parquet | ✅ | ❌ | ❌ | rarely |
138
+ | Checksums + verify against the live org | ✅ | ❌ | ❌ | varies |
139
+ | Scriptable / CI-friendly | ✅ | ❌ | partly | API only |
140
+ | Data never leaves your machine | ✅ | ✅ | ✅ | ❌ stored by vendor |
141
+ | Price | **free, MIT** | included | free | $$ per user / month |
142
+
143
+ Use it to **migrate** to another CRM, **archive** an org before you shut it down, take **independent backups**, or feed a **data warehouse**.
144
+
145
+ ## Features
146
+
147
+ - **Complete by default.** It finds every queryable object (standard, custom, history, activities, Chatter, email, custom metadata) and every field. It also explains anything it skips in `REPORT.md`.
148
+ - **Fast on big orgs and light on API limits.** Large objects go through **Bulk API 2.0**, several at a time. Small objects are described and fetched **25 per API call** using composite requests, so a typical org's hundreds of small objects cost dozens of calls, not a thousand. If Bulk refuses an object, it falls back to REST.
149
+ - **Handles Salesforce's quirks.** Knowledge articles (`__kav`, which need a status filter) and `ContentDocumentLink` (which needs an Id filter) are exported anyway. System objects that can't be exported are skipped with the reason, instead of failing your run.
150
+ - **Files that stay connected.** ContentVersion, Attachment and Document bodies are downloaded in parallel, checked against Salesforce's own MD5 checksum, and mapped back to their records via `ContentDocumentLink` and `files/index.csv`.
151
+ - **Resumable.** Press Ctrl-C, lose Wi-Fi or hit an API limit, then run the same command again. Finished objects and files are skipped.
152
+ - **Deleted and archived records** with `--include-deleted` (Recycle Bin, archived activities).
153
+ - **Real schemas.** Typed DDL for Postgres, DuckDB and SQLite, plus a Mermaid ER diagram. `sf-exit load` creates typed tables: dates are dates, booleans are booleans, currency is numeric.
154
+ - **Verifiable.** `sf-exit verify` re-counts every CSV and re-hashes every file. `--live` compares counts against the org.
155
+ - **Read-only and private.** sf-exit never modifies your org and has no telemetry. Data goes straight from Salesforce to your disk.
156
+ - **Plays well with others.** It's a plain folder of CSVs. Also available as a pip package, a Docker image and a GitHub Action.
157
+
158
+ ## Usage
159
+
160
+ ### Authentication
161
+
162
+ | Method | Command | Best for |
163
+ |---|---|---|
164
+ | Salesforce CLI | `sf-exit export --org my-alias` | Laptops. Zero setup. If you omit `--org`, your default CLI org is used |
165
+ | Access token | `--instance-url https://acme.my.salesforce.com --access-token 00D…` | One-offs, other tools |
166
+ | OAuth client credentials | `--instance-url … --client-id … --client-secret …` | Servers, CI ([setup](docs/authentication.md#client-credentials)) |
167
+ | JWT bearer | `--client-id … --username … --jwt-key-file server.key` | CI with a certificate (`pip install 'sf-exit[jwt]'`) |
168
+ | Username + password | `--client-id … --client-secret … -u … --password 'pw+token'` | Legacy setups |
169
+
170
+ Every flag can also be set as an environment variable (`SF_ORG`, `SF_INSTANCE_URL`, `SF_ACCESS_TOKEN`, `SF_CLIENT_ID`, `SF_CLIENT_SECRET`, `SF_USERNAME`, `SF_PASSWORD`, `SF_JWT_KEY_FILE`). Add `--sandbox` for test.salesforce.com logins. More detail is in [docs/authentication.md](docs/authentication.md).
171
+
172
+ > **Permissions:** for a complete export the user should have **View All Data** and **Query All Files**. Without them, Salesforce silently returns only the records and files that user can see. `sf-exit doctor` checks this for you.
173
+
174
+ ### Choosing what to export
175
+
176
+ ```bash
177
+ sf-exit export -i Account -i Contact -i 'Invoice*__c' # only these (globs, case-insensitive)
178
+ sf-exit export -x '*History' -x '*Share' -x '*Feed' # everything except these
179
+ sf-exit export --include-deleted # + Recycle Bin and archived records
180
+ sf-exit export --no-files # records only
181
+ sf-exit export --files-only --all-file-versions # every version of every file
182
+ ```
183
+
184
+ ### Loading into a database
185
+
186
+ ```bash
187
+ sf-exit load ./export --to sqlite:///salesforce.db # built in
188
+ sf-exit load ./export --to duckdb:///salesforce.duckdb # pip install 'sf-exit[duckdb]'
189
+ sf-exit load ./export --to parquet:///./parquet # pip install 'sf-exit[parquet]'
190
+ sf-exit load ./export --to postgresql://me:pw@localhost/crm --lowercase # pip install 'sf-exit[postgres]'
191
+
192
+ sf-exit export --org my-org --load duckdb:///salesforce.duckdb # export + load in one go
193
+ ```
194
+
195
+ Use `pip install 'sf-exit[all]'` to get every extra.
196
+
197
+ ### Verifying an export
198
+
199
+ ```bash
200
+ sf-exit verify ./export # offline: CSV row counts + SHA-256 of every data file and binary file
201
+ sf-exit verify ./export --live # also compare row counts against the org right now
202
+ ```
203
+
204
+ ### Docker
205
+
206
+ ```bash
207
+ docker run --rm -it -v "$PWD:/work" -e SF_INSTANCE_URL -e SF_ACCESS_TOKEN \
208
+ ghcr.io/akhsterminator/sf-exit export -d /work/export
209
+ ```
210
+
211
+ ## Nightly backups with GitHub Actions
212
+
213
+ sf-exit ships as a GitHub Action. Put this in a **private** repository and you have free, versioned, off-platform Salesforce backups:
214
+
215
+ ```yaml
216
+ # .github/workflows/salesforce-backup.yml
217
+ on:
218
+ schedule: [{ cron: "0 2 * * *" }]
219
+ workflow_dispatch:
220
+ jobs:
221
+ backup:
222
+ runs-on: ubuntu-latest
223
+ steps:
224
+ - uses: akhsterminator/sf-exit@v0
225
+ with:
226
+ args: export -d sf-export --load parquet:///sf-parquet
227
+ env:
228
+ SF_INSTANCE_URL: ${{ secrets.SF_INSTANCE_URL }}
229
+ SF_CLIENT_ID: ${{ secrets.SF_CLIENT_ID }}
230
+ SF_CLIENT_SECRET: ${{ secrets.SF_CLIENT_SECRET }}
231
+ - uses: actions/upload-artifact@v7
232
+ with: { name: salesforce-backup, path: sf-export, retention-days: 30 }
233
+ ```
234
+
235
+ There's a fuller example, including upload to S3, in [examples/github-actions-backup.yml](examples/github-actions-backup.yml).
236
+
237
+ ## How it works
238
+
239
+ ```mermaid
240
+ flowchart LR
241
+ A[describeGlobal] --> B[describe each object<br/>skip compound & blob fields]
242
+ B --> C{rows ≥ 10k?}
243
+ C -- yes --> D[Bulk API 2.0 query job<br/>stream CSV pages]
244
+ C -- no --> E[REST query<br/>paginate JSON → CSV]
245
+ D -. refused .-> E
246
+ D & E --> F[data/*.csv + state.json]
247
+ F --> G[ContentDocumentLink<br/>chunked by document Id]
248
+ G --> H[download file bodies<br/>in parallel, MD5-verified]
249
+ H --> I[schema.sql · erd.mmd · manifest.json · REPORT.md]
250
+ I --> J[(sf-exit load →<br/>SQLite / DuckDB / Postgres / Parquet)]
251
+ ```
252
+
253
+ ## FAQ
254
+
255
+ <details><summary><b>Will this use up my API limits?</b></summary>
256
+
257
+ Usually not by much. A Bulk API job costs a handful of calls whatever its size. Small objects are fetched 25 per call, and each file costs one call. `sf-exit plan` estimates the total and shows your remaining daily allowance before you start. Use `--workers 1` to go gently on a busy org.
258
+ </details>
259
+
260
+ <details><summary><b>How long does it take?</b></summary>
261
+
262
+ It depends mostly on Salesforce's Bulk API and your file storage. As a rough guide, millions of records take minutes to tens of minutes, and file downloads are limited by bandwidth. Because exports resume, a long run can be spread over several sessions.
263
+ </details>
264
+
265
+ <details><summary><b>Does it export metadata (Apex, flows, layouts, reports)?</b></summary>
266
+
267
+ Not yet. sf-exit focuses on **data**, but it does save each object's schema (fields, picklists, relationships) as JSON and SQL. For metadata, use `sf project retrieve start --manifest package.xml` today. Native support is on the [roadmap](#roadmap).
268
+ </details>
269
+
270
+ <details><summary><b>Which editions are supported?</b></summary>
271
+
272
+ Any org with API access: Enterprise, Unlimited, Performance and Developer editions, plus Professional Edition with the API add-on. Sandboxes work with `--sandbox` or a sandbox CLI alias.
273
+ </details>
274
+
275
+ <details><summary><b>Why are some objects skipped?</b></summary>
276
+
277
+ Some "objects" are really metadata views (`FieldDefinition`), virtual roll-ups of data exported elsewhere (`OpenActivity`, `CombinedAttachment`), streaming events (`*ChangeEvent`), external objects (`__x`) or big objects (`__b`). Every skip and its reason is listed in `REPORT.md`. Run with `-v` to see them in `sf-exit plan`.
278
+ </details>
279
+
280
+ <details><summary><b>What about compound fields like addresses?</b></summary>
281
+
282
+ The Bulk API can't return compound fields, so sf-exit exports their components instead: `BillingStreet`, `BillingCity`, `BillingPostalCode` and so on. No data is lost.
283
+ </details>
284
+
285
+ <details><summary><b>Is my data sent anywhere?</b></summary>
286
+
287
+ No. sf-exit talks only to your Salesforce instance and writes only to your disk. There's no telemetry, no account and no server.
288
+ </details>
289
+
290
+ <details><summary><b>Can I put the data back into Salesforce, or into another CRM?</b></summary>
291
+
292
+ The export keeps every original Id and relationship column, which is exactly what you need to rebuild links in a target system. Guided re-import into another org, HubSpot or Attio is on the roadmap, and recipes are very welcome as PRs.
293
+ </details>
294
+
295
+ ## Roadmap
296
+
297
+ - [ ] Incremental exports (only changed rows, via `SystemModstamp`)
298
+ - [ ] Metadata export (Apex, flows, layouts, reports) via the Metadata API
299
+ - [ ] Write straight to S3 / GCS / Azure Blob
300
+ - [ ] Import helpers: Salesforce → another Salesforce org, HubSpot, Attio, Twenty
301
+ - [ ] Field-level PII redaction for sharing exports safely
302
+ - [ ] Big object (`__b`) export
303
+ - [ ] Single-file binaries (Homebrew, Scoop)
304
+
305
+ Have an idea or need? [Open an issue](https://github.com/akhsterminator/sf-exit/issues/new/choose). Good first issues are [labelled](https://github.com/akhsterminator/sf-exit/labels/good%20first%20issue).
306
+
307
+ ## Contributing
308
+
309
+ Contributions are welcome! The whole test suite runs against an in-process **fake Salesforce org** (`tests/fake_org.py`), so you don't need a Salesforce account to hack on sf-exit:
310
+
311
+ ```bash
312
+ git clone https://github.com/akhsterminator/sf-exit && cd sf-exit
313
+ pip install -e '.[dev]'
314
+ pytest
315
+ ```
316
+
317
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
318
+
319
+ ## Star history
320
+
321
+ If sf-exit saved you a migration project or a backup bill, a ⭐ helps other people find it.
322
+
323
+ [![Star History Chart](https://api.star-history.com/svg?repos=akhsterminator/sf-exit&type=Date)](https://star-history.com/#akhsterminator/sf-exit&Date)
324
+
325
+ ## License
326
+
327
+ [MIT](LICENSE). sf-exit is an independent open-source project and is not affiliated with, endorsed by or sponsored by Salesforce, Inc. Salesforce is a trademark of Salesforce, Inc.