Quickstart — run the emulator on your own org
Run your real Apex triggers against a fake Salesforce org on localhost, inject failures a real org never lets you reproduce, and test unapproved changes before they ship.
Run your real Apex triggers against a fake Salesforce org on localhost, and inject
failures a real org will never let you reproduce (row locks, API limits). No Go, no
compilers — this bundle ships prebuilt binaries.
This bundle contains:
sfemu— the emulator serversfapex— the analyzer (tells you what will run before you run it)- this
QUICKSTART.md
You need the Salesforce CLI
(sf) — you almost certainly already have it — and your org checked out as an SFDX
project (force-app/…).
macOS will block the binaries the first time (unsigned). Clear it once:
xattr -d com.apple.quarantine ./sfemu ./sfapex— or right-click each → Open.
1. Convert your source once (REQUIRED for a full org)
The emulator knows two objects out of the box (Account, Contact). Every other
object — standard or custom — it learns from your metadata. Your SFDX checkout is
decomposed; the emulator reads MDAPI .object files. One convert produces both the
Apex and the object metadata:
cd /path/to/your/sfdx-project
sf project convert source --root-dir force-app --output-dir mdapi
# -> mdapi/classes/*.cls mdapi/triggers/*.trigger mdapi/objects/*.object
Objects not in your source won’t exist in the emulator — if a trigger targets one, add it to your project and re-convert.
2. See what will run — before running it
./sfapex coverage mdapi
Per-trigger verdict, no surprises: EXECUTE (runs for real), HOOKS (approximated),
UNRESOLVED (refused, with file:line + reason). Handler-instance / inheritance style
triggers execute; config-driven dispatch (NPSP TDTM-style) shows UNRESOLVED today — an
honest gap, not a crash. --json for machine-readable.
3. Run the emulator on your org
# one flag — point --apex-src at your converted mdapi dir; it supplies both the
# triggers/classes AND the object schema (from mdapi/objects):
./sfemu --addr :8080 --apex-src mdapi
# the explicit equivalent (env var + metadata dir), if you prefer:
SFEMU_APEX_SRC=mdapi ./sfemu --addr :8080 --metadata-dir mdapi/objects
It logs its version and how many triggers loaded/EXECUTE-classified, then answers the
Salesforce REST API at http://localhost:8080. Point your integration or tests there
instead of a sandbox.
4. Talk to it — your triggers fire
curl -sX POST localhost:8080/services/data/v62.0/sobjects/Opportunity \
-H 'Content-Type: application/json' \
-d '{"Name":"Big Deal","StageName":"Prospecting","Amount":50000}'
curl -s localhost:8080/services/data/v62.0/sobjects/Opportunity/<id>
curl -s --get localhost:8080/services/data/v62.0/query \
--data-urlencode "q=SELECT Id, Name FROM Opportunity WHERE Amount > 0"
For EXECUTE-classified objects the REST layer is permissive about fields, so triggers reading/writing standard fields not present in your metadata still work.
5. Break it on command (the whole point)
| env var | effect |
|---|---|
SFEMU_FAULT_LOCK_ON_UPDATE=true | every PATCH returns UNABLE_TO_LOCK_ROW |
SFEMU_FAULT_LIMIT_AFTER=N | after N writes, return REQUEST_LIMIT_EXCEEDED |
SFEMU_LATENCY_MS=N | add N ms latency to every response |
The boolean flags accept any truthy value — 1, true, t, TRUE. Faults fire on
every object identically, whether it runs through the interpreter or the hooks path.
SFEMU_FAULT_LOCK_ON_UPDATE=true SFEMU_APEX_SRC=mdapi ./sfemu --addr :8080 --metadata-dir mdapi/objects
# curl -sX PATCH .../sobjects/Account/001... -> {"errorCode":"UNABLE_TO_LOCK_ROW", ...}
Your retry/error-handling path — normally impossible to test — now has something real to catch.
Testing unapproved changes — boot the future org
You don’t have to wait for a change to be approved and deployed to see what it does. The emulator learns the org from metadata on disk, so you can run proposed Apex against the org as it would be once the change lands — locally, before review. The only variable is which source tree you convert:
-
Base snapshot. Start from the source that represents the current, approved org (a clean checkout /
main). This is your baseline.cd /path/to/sfdx-project sf project convert source --root-dir force-app --output-dir mdapi -
Overlay your unapproved change. Drop the new/edited
.clsand.triggerfiles into the source tree (check out your feature branch, or copy them in):cp AccountDedupe.trigger force-app/main/default/triggers/ cp AccountDedupeHandler.cls force-app/main/default/classes/ -
Re-convert.
mdapi/now describes the org with your change applied — the future org:sf project convert source --root-dir force-app --output-dir mdapi -
See what it’ll do, then boot it:
./sfapex coverage mdapi # is the new trigger EXECUTE / HOOKS / UNRESOLVED? SFEMU_APEX_SRC=mdapi ./sfemu --addr :8080 --metadata-dir mdapi/objects
The emulator is now the org as it would be after your change deploys. Exercise it with the REST calls from steps 4–5 above (including faults) — you’re testing the future org without touching a sandbox or waiting on approval.
Want present-vs-future side by side? Convert the approved tree to
mdapi-base/and the overlaid tree tomdapi-next/, boot each on a different--addr, and compare the two orgs directly.
What talks to the network
- The emulator makes no outbound calls. No telemetry, no phone-home. It reads your
local
mdapi/folder and serves REST onlocalhost. Run it offline and it behaves identically. sf project convert sourceis local — it just reshapes files on disk.- The only thing that ever reaches Salesforce is your own
sfCLI, using your credentials against your org — never ours, never us. (If you’d rather pull fresh from a live org than use a local checkout,fidelic sync --org <alias>wrapssf project retrievewith your credentials.)
Notes
- Versions:
./sfemu versionand./sfapex versionprint the build SHA — quote them when you report anything, so it’s traceable to an exact build. - Persistence:
--db :memory:(default) is wiped on restart;--db ./org.sqlitekeeps data across runs. - Auth: off by default.
SFEMU_CLIENT_SECRET=...requires a client secret on the token endpoint. - What’s REFUSED won’t run — by design.
sfapex coverage(step 2) tells you exactly what, up front, so there are no lies at runtime. - Linux binaries are glibc-dynamic (standard distros / CI). Not built for Alpine/musl yet.