platform-datamask-run
Data Mask end-to-end operation on a sandbox: configure a masking policy over PII, run the masking job, poll it to completion, report masked-record results, and abort an in-progress run. Use when the user needs to run, monitor, or cancel a Salesforce Data Mask job, mask PII/sandbox data, or work with
By forcedotcom · 1,192 installs
npx skills add forcedotcom/sf-skills --skill platform-datamask-run
Source repository · Upstream listing
platform datamask run: Salesforce Data Mask End to End Operation
Use this skill to operate the Salesforce Data Mask feature on a sandbox : configure a
masking policy over PII fields, start a masking job, poll it to a terminal state, report which
records were masked, and abort a run that is still in progress.
Data Mask is sandbox only — the run/abort REST endpoints return 403 on production (a runtime
sandbox guard). Confirm the target org is a sandbox before starting.
When This Skill Owns the Task
Running a Data Mask job against a configured policy
Polling masking job status to completion
Reporting masked record counts / per object results
Aborting (canceling) an in progress masking run
Creating or identifying the policy the job runs against
Delegate elsewhere when the user is:
hand writing anonymization Apex → platform apex generate
seeding or generating test data → platform data manage
deploying unrelated metadata → platform metadata deploy
The One Thing to Get Right First: the API surface map
The single biggest failure mode is assuming Data Mask entities are ordinary data API objects.
They are not, and the surface differs per entity. Memorize this table before running anything —
guessing here is what turns a 3 second job into a 30 minute dead end.
Entity What it is How you reach it
DataMaskPolicy The masking policy shell (config) Tooling API or Metadata API (thin shell: <label / <description / <runOnRefresh only) — NOT standard SOQL/ sobject describe
DataMaskPolicyObject An object targeted by a policy (holds the optional row filter) Tooling API only — query AND insert; row subset "sample" runs set FilterEnabled + WhereCriteria here (no sampleSize on the policy)
DataMaskPolicyField A field + its masking treatment Tooling API only — query AND insert; treatment cols are MaskingCategory + MaskValue
DataMaskPolicyJobRun The job (one masking run) Standard SOQL — sf data query works
DataMaskPolicyJobRunDtl Per object job detail (child, FK DataMaskPolicyJobRunId ) Standard SOQL
Start a run — REST run API POST /services/data/v67.0/platform/data resilience/data mask/policies/{policyId}/run
Abort a run — REST run API POST /services/data/v67.0/platform/data resilience/data mask/jobs/{jobRunId}/abort
Concretely:
sf sobject describe sobject DataMaskPolicy → NOT FOUND (don't retry it against standard API)
SELECT ... FROM DataMaskPolicy via sf data query → INVALID TYPE
Query the policy via Tooling: sf data query use tooling api query "SELECT Id, MasterLabel FROM DataMaskPolicy"
Query the job / job detail via standard API: sf data query query "SELECT Id, Status FROM DataMaskPolicyJobRun"
Full command reference: references/api surface.md .
Pick the workflow that matches the request
This skill has two distinct workflows . Select ONE up front from what the user asked for, then
run every step of that workflow — neither has optional steps:
The user wants to… Run Ends when
Configure/edit a policy and mask records; report how many were masked Workflow A — Mask & report (below) The masked count is reported from the detail rows
Cancel / abort a masking run Workflow B — Cancel a run (further below) The job's status is confirmed canceled
Choose by the verb in the request. "Create/edit a policy and run it", "mask the PII", "how many
records were masked" → Workflow A only . "Abort", "cancel", "stop the run" → Workflow B . A
mask and report request does not include an abort: do not start a second job to "demonstrate"
cancelling — an unrequested run wastes a full ~5–10 min job (see the pool floor in A4) and is the top
cause of this task running out of turn before it finishes the masked count it was asked for.
Workflow A — Mask & report
A1. Confirm sandbox + capture org context
Verify the org is a sandbox and grab the instance URL + a session token for the run API calls:
A2. Identify or create the policy
Prefer reusing an existing policy (fastest, no deploy):
If none targets the Contact PII you need, author one with the two step recipe (the
DataMaskPolicy Metadata shape is a thin shell; membership is Tooling inserted):
1. Metadata deploy the thin shell in mdapi format ( metadata dir + package.xml ; a
source format source dir deploy fails "Could not infer a metadata type"). The shell carries
only <label , <description , <runOnRefresh . This creates the policy with an active
revision , which A2 requires.
2. Tooling insert the DataMaskPolicyObject (one per object) then its DataMaskPolicyField
rows. Each field row's treatment is MaskingCategory ( library ) + MaskValue (a snake case
token like first name , email , phone ). There is no MaskingRuleType column .
Insert order matters: a Tooling created parent (no active revision) makes the child insert fail
INSUFFICIENT ACCESS ON CROSS REFERENCE ENTITY . Metadata deploy the shell first.
See references/policy authoring.md for the full recipe and the MaskValue token table. Choose a
MaskValue appropriate to each field; do not blanket replace.
A3. Start the masking run (REST run API)
The endpoint needs an empty JSON body ( {} ) — sf api request rest requires body on a POST
even when the API takes no payload. Pass the file with an @ prefix ( body @./empty body.json );
without it the literal path is sent as the body and the API returns JSON PARSER ERROR . A 200 returns jobRunId , policyId , status (the run API
status is UPPERCASE, e.g. RUNNING ) and message: "Job started successfully" . A 409 / CONFLICT
means a run is already in progress for that policy.
Write report.md NOW, before you poll — do not wait until the end. The masking job takes
several minutes (see below), and the single most common way this task scores zero is the turn
ending during the poll with no output file written at all . The instant you have the jobRunId ,
write report.md with everything known so far (policy Id/label, the run command, the jobRunId ,
status RUNNING , and a "polling for completion…" placeholder for the masked count). Then update
that same file once the job finishes. A report that exists and says "still running" beats no file;
a fabricated count is worse than either — only fill the count from the detail rows (A5).
A4. Poll to a terminal state (standard SOQL)
Poll DataMaskPolicyJobRun.Status until it reaches a terminal value. Do not report a
mid run status as final.
Mid run (pre work): pending , scheduled — the job is queued but not yet abortable
Mid run (working): running — this is the only state in which abort succeeds
Terminal: completed , completed with errors , failed
Abort target: canceled (single "l")
pending is not running . Abort on a pending / scheduled job returns 409 CONFLICT
("Job is not in a running state ... status=PENDING"). You must poll until the status is literally
running before you can abort — see Workflow B.
Jobs are slow — expect several minutes, and poll with the bundled script. Data Mask runs on a
backend pool/scheduler with a ~5–10 minute floor : even a tiny (20 row) job usually does not
reach a terminal state or emit detail rows for several minutes after the run starts. This is fixed
overhead, not proportional to row count. Plan the run around it — the single biggest failure mode
is treating the job as instant, polling on a tight interval, and either timing out or writing a
"still pending" report.
Run scripts/poll job.sh as a single command — do not hand roll a SOQL poll loop:
It sleeps on a low frequency interval, short circuits the instant a ground truth detail row appears,
prints the terminal signal ( completed / failed / canceled ) on stdout, and exits 0 (or 1 on
timeout). Call it once and read its result — do not wrap it in your own retry loop , and do not
poll on a sub 10s interval (it just burns tool calls against a job that cannot finish sooner).
Ground truth is the detail rows, not the parent status. The parent DataMaskPolicyJobRun.Status
can lag — it may read pending / running for a while after masking actually finished. Once a
total records masked (or completed ) DataMaskPolicyJobRunDtl row exists, the masking is done.
poll job.sh already encodes all of this — the bounded interval and timeout, the short circuit on
the ground truth detail row, and the terminal signal exit code — so you do not re implement any
of it inline. Run the poller once, read its exit signal, then update report.md (the stub you wrote
before polling) with the terminal status and the masked count from A5.
A5. Report results from the job DETAIL object
The parent job carries an overall status; per object masked counts live on the child
DataMaskPolicyJobRunDtl (linked by DataMaskPolicyJobRunId ). Report a concrete count, not a
fabricated one:
Report only what the rows literally show — do not overstate granularity. The detail rows are
object level status update entries ( loaded , completed , total records masked for the object,
e.g. Contact). They are not per field rows. So state per object success as an observed fact
("Contact: 27/27 records masked, 0 error rows"), but frame field level success as an inference ,
not a direct observation — say "no field level error rows were returned, so no field is reported as
failed", not "all 5 fields succeeded" (the data does not carry a per field success row to back
that claim). Overstating an inference as an observation is the most common factuality miss here.
Workflow B — Cancel a run
Use this workflow when the request is to abort/cancel a masking run. It targets the run that is
currently in progress — aborting is an on demand action against a live job; nobody starts a job
just to cancel it. Steps B1–B4 are all required.
B1. Confirm sandbox + identify the run to cancel
Confirm the org is a sandbox ( sf org display ) and get the jobRunId of the run to abort — the one
the user is asking to cancel. Capture its DataMaskPolicyId too — you need it to start a
replacement run if the abort window is missed (B2 exit 3 / exit 1). If they just started it, use that
id; otherwise query for the active run:
Note the DataMaskPolicyId ( 8dm prefix) of the run you pick — that is the <policyId A3 needs.
B2. Wait for the job to be running (the only abortable state)
You can only abort while DataMaskPolicyJobRun.Status is running . A pending / scheduled job
409 s; a terminal one is already done. Poll for the running window with the bundled poller in its
running mode — it exits the instant the status reads running (unlike the default mode, which
waits for a terminal state), so it will not block past the abortable window:
The cap is 900s (15 min) , above the ~5–10 min scheduling floor so a slow to start job still gets
caught. Handle every exit:
Exit 0 (prints running ) → go straight to B3.
Exit 3 → the job raced to a terminal state before running was caught; the abort window is
gone. Start a fresh run against the policy you captured in B1 (A3 with that <policyId ), then
return here and poll the new jobRunId .
Exit 1 (timeout — the cap expired) → re query the job's status:
If it is still non terminal ( pending / scheduled / running ), re run the poller once more (same
command) to continue waiting. If it is running , go to B3. If it is terminal, treat it like exit 3
— start a fresh run (A3 with the B1 <policyId ) and poll the new job.
Because of the ~5–10 min pool floor the running window is usually minutes wide, so there is time to
catch it; do not poll with no delay.
If no run is currently in progress (the job already completed, or you must reproduce a
run→cancel flow end to end), start