← Docs

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). One Docker image, nothing to install, nothing to compile.

docker pull fidelic/emulator

That image contains two binaries, both stamped with the same build SHA:

binarywhat it ishow you run it
/sfemuthe emulator serverthe default entrypoint
/sfapexthe analyzer — tells you what will run before you run it--entrypoint /sfapex

Run it with no arguments and it prints this cheat-sheet and exits; it does not start an empty org.

You need the Salesforce CLI (sf) — you almost certainly already have it — and your org checked out as an SFDX project (force-app/…).


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

docker run --rm -v "$PWD/mdapi":/mdapi:ro \
  --entrypoint /sfapex fidelic/emulator coverage /mdapi

Per-trigger verdict, no surprises: EXECUTE (no blocker found — the interpreter will attempt it), HOOKS (approximated), UNRESOLVED (refused, with file:line + reason), RECORDED-ONLY (the effect is captured, never really sent). An EXECUTE* row carries a SIMULATED: note naming which effects are captured rather than performed. Handler-instance / inheritance style triggers are the ones that classify EXECUTE most often; config-driven dispatch (NPSP TDTM-style) shows UNRESOLVED today — an honest gap, not a crash. --json for machine-readable.

EXECUTE is a static verdict, not a guarantee. It means the classifier walked the reachable code and found nothing it knows it cannot run — it does not mean we have run this trigger. On our own 14-repo reference corpus, 46 triggers classify EXECUTE and 8 enter their body in every context they declare when actually driven over REST (5 more enter some). The rest fail on constructs the classifier admitted; each one is a bug we hold. Treat EXECUTE as “worth trying”, run it, and read what actually happens.

This step runs entirely offline — --network none works — and its report is the one artifact worth sending back to us. See release/WHAT-LEAVES-YOUR-MACHINE.md for exactly what that report does and does not contain.

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):
docker run -p 8080:8080 -v "$PWD/mdapi":/mdapi:ro fidelic/emulator --apex-src /mdapi

# the explicit equivalent (env var + metadata dir), if you prefer:
docker run -p 8080:8080 -v "$PWD/mdapi":/mdapi:ro \
  -e SFEMU_APEX_SRC=/mdapi fidelic/emulator --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.

Boot is proportional to how much Apex you have. A handful of triggers is well under a second; a large real org takes longer — the Nonprofit Success Pack’s 26 triggers and ~1,000 classes take about 15 s on a small cloud VM. It is parsing every class you gave it.

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 vareffect
SFEMU_FAULT_LOCK_ON_UPDATE=trueevery PATCH returns UNABLE_TO_LOCK_ROW
SFEMU_FAULT_LIMIT_AFTER=Nafter N writes, return REQUEST_LIMIT_EXCEEDED
SFEMU_LATENCY_MS=Nadd N ms latency to every /services/data/ response (token + /__emulator__/ calls are unaffected)

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.

docker run -p 8080:8080 -v "$PWD/mdapi":/mdapi:ro \
  -e SFEMU_FAULT_LOCK_ON_UPDATE=true fidelic/emulator --apex-src /mdapi

A PATCH then returns HTTP 400 with the real Salesforce error shape — an array:

[{"message":"unable to obtain exclusive access to this record or 1 records",
  "errorCode":"UNABLE_TO_LOCK_ROW","fields":[]}]

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:

  1. 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
  2. Overlay your unapproved change. Drop the new/edited .cls and .trigger files 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/
  3. 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
  4. See what it’ll do, then boot it:

    # is the new trigger EXECUTE / HOOKS / UNRESOLVED?
    docker run --rm -v "$PWD/mdapi":/mdapi:ro \
      --entrypoint /sfapex fidelic/emulator coverage /mdapi
    
    docker run -p 8080:8080 -v "$PWD/mdapi":/mdapi:ro fidelic/emulator --apex-src /mdapi

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 to mdapi-next/, boot each on a different host port, and compare the two orgs directly.


What talks to the network

  • The emulator makes no outbound calls. No telemetry, no phone-home, no licence check. It reads the mdapi/ folder you mounted and serves REST on localhost. Run it with --network none and it behaves identically.
  • sf project convert source is local — it just reshapes files on disk.
  • The only thing that ever reaches Salesforce is your own sf CLI, using your credentials against your org — never ours, never us.
  • --demo npsp is the one exception, and it is opt-in: it clones a public corpus from GitHub at a pinned commit, once, and caches it. Nothing of yours leaves. Without that flag the image never opens a socket outbound.

Notes

  • Versions: both binaries print the same build SHA — quote it when you report anything, so it’s traceable to an exact build:

    docker run --rm fidelic/emulator version                      # sfemu  <sha>
    docker run --rm --entrypoint /sfapex fidelic/emulator version # sfapex <sha>
  • Persistence: --db :memory: (default) is wiped on restart. To keep data across runs, mount a directory and point at a file inside it: -v "$PWD/data":/data --db /data/org.sqlite.

  • Auth: off by default. SFEMU_CLIENT_SECRET=... requires a client secret on the token endpoint.

  • What’s UNRESOLVED won’t run — by design. sfapex coverage (step 2) tells you exactly what, up front, so there are no lies at runtime.

  • Try it without any of this: https://play.fidelic.dev boots a real NPSP org in your browser, no install and no signup.

  • fidelic sync (scripts/fidelic) is a small shell wrapper around sf project retrieve for pulling metadata straight from a live org with your credentials. It is a host-side script, not part of the image, because it drives your local sf CLI.