ApexGuru Performance Scan Skill
CRITICAL: Mandatory Script Usage
Every step — token resolution, zipping, API calls, and report decoding — MUST go
through the bundled scripts in
. No exceptions.
WRONG — never do this:
bash
# WRONG: hand-rolled curl to the API
curl -X POST https://api.salesforce.com/... -F file=@x.zip
# WRONG: inline base64 + jq to read the report
cat raw.json | jq -r .report | base64 -d | jq '.[]'
# WRONG: reading the raw result file directly (report is a large base64 blob)
Read tool → apexguru-raw-*.json
# WRONG: inline node/python to parse violations
node -e "const r = require('./raw.json'); ..."
RIGHT — always do this:
bash
# PREFERRED — one command runs all three steps (package → submit+poll →
# decode+present) and prints the ready-to-show report as its final stdout.
# Use this for every initial scan: it cannot be left half-finished.
bash "<skill_dir>/scripts/scan.sh" "<project-root>"
# Optionally persist the presented markdown to a file as well:
bash "<skill_dir>/scripts/scan.sh" "<project-root>" --out ./apexguru-report.md
The three underlying scripts still exist and
calls them in order.
Invoke them individually only for
drill-downs on an already-scanned result
(Step 5), or when you deliberately need to inspect an intermediate artifact:
bash
# Equivalent manual chain (scan.sh runs exactly these, in this order):
bash "<skill_dir>/scripts/build-zip.sh" "<project-root>" "./apexguru-<TS>.zip"
bash "<skill_dir>/scripts/run-scan.sh" "./apexguru-<TS>.zip" "./apexguru-raw-<TS>.json"
node "<skill_dir>/scripts/decode-report.js" "./apexguru-raw-<TS>.json" --present
# Drill into a subset WITHOUT re-scanning (reuse the raw file scan.sh left, or
# pass --raw to scan.sh to keep it at a known path):
node "<skill_dir>/scripts/decode-report.js" "./apexguru-raw-<TS>.json" --rule SOQL_IN_LOOP --full
node "<skill_dir>/scripts/decode-report.js" "./apexguru-raw-<TS>.json" --group file --top 5
is the absolute path to the directory containing this SKILL.md.
Never use
— that resolves against the user's CWD, not the skill dir.
Any filter/rank/group question ("which file has the most issues?", "show only
SOQL-in-loop", "break down by severity") is answered by re-running
with flags against the
same raw result file — never re-scan,
never parse the JSON by hand.
CRITICAL: Present output verbatim — never condense it
decode-report.js --present
(Step 4) already produces the final, ready-to-show
markdown: severity legend, one detail card per violation (message, code, fix,
resource link), and a closing summary table. That stdout
is the response.
Print it to the user exactly as printed — do not rewrite it into a shorter
table, do not drop the per-issue cards down to just the summary table, and do
not wait for the user to ask "explain a violation" before including
message/fix/resource. Condensing it defeats the entire point of
.
The attribution is already in that stdout — the summary line is the exact
output that states the mode (e.g. "ApexGuru (static analysis) is active. To
unlock runtime intelligence…"). Do NOT prepend or append your own attribution sentence
(no "Attribution: analysisMode: static…", no naming the org, no restating
"static-only findings"). The script's line is the complete, approved wording;
adding your own makes the output non-deterministic and off-message.
WRONG — never do this:
text
Top Issues (worst first)
# Severity Rule Method Line
1 Major UsingTheTestMethodKeyword legacy... 136
...
Key Antipatterns Detected:
- SOQL/DML in loops (3 violations)
(a hand-built summary that drops every message/code/fix — even for
violations that had one)
text
Attribution: analysisMode: static — source-only analysis. The scanned org
(ag-skills-org) is not onboarded to ApexGuru's full runtime metrics, so
these are static-only findings.
(an agent-authored attribution line prepended to the report — the script's
own summary line already states the mode; this duplicate is non-deterministic
and names an org the script never had access to)
RIGHT — always do this:
Paste the full stdout from
decode-report.js --present
— every
card and the closing
table — unedited, in one response.
Overview
ApexGuru detects
performance antipatterns in Apex (SOQL/DML in loops,
Schema.getGlobalDescribe()
, SOQL without
/
, unused SOQL fields).
This skill drives the ApexGuru
SFAP Scan API: it packages the user's Apex
(every
/
under the project root, any layout) into a zip, submits
it, polls until the scan finishes, decodes the base64-encoded report, and
presents violations grouped by rule with severity,
, and suggested
fixes.
Attribution is mandatory. The API returns
:
- → source-only analysis → label results "Static only".
- → enriched with runtime metrics from an org onboarded to ApexGuru →
label results "Production insights".
decode-report.js --present
already renders this attribution into its summary
line and title ("Static only" / "Production insights") — that satisfies the
mandatory-attribution requirement. Print that line as the
exact output; do
not
author your own attribution sentence or name the org. If the user expected
but got
, the script's static-mode line already explains the org
is not onboarded — point them to it rather than restating it (see error handling).
In scope: zipping a project's Apex, submitting/polling the scan, decoding + presenting
violations, filtering/grouping existing results, troubleshooting API errors.
Out of scope: general static analysis / security / lint (→
,
which lists ApexGuru as an engine), applying fixes to code, onboarding an org to
ApexGuru, minting SFAP tokens.
Prerequisites
- An authenticated CLI org ().
derives the SFAP JWT from it via — this is the normal
IDE-session path. Alternatively, set /
to supply a JWT directly (CI/headless). The org is derived from the token's
claim — no org id is passed. Pass to pick a specific org.
See
<skill_dir>/references/authentication.md
. If no token can be resolved, the
script returns a clear error with a hint.
- , , , , , on PATH (standard on macOS/Linux dev boxes).
- A folder containing Apex — an sfdx project, a subtree, or any
folder with / files. collects all Apex beneath
it regardless of layout; the API walks the whole archive.
Workflow
Step 1: Identify the project root
The project root is any folder that
contains Apex somewhere beneath it
(usually an sfdx project root next to
, but a
subtree or a loose folder of
files works too). If the user gave a path,
use it; otherwise use the current working directory.
collects
every
/
under it (any layout) and fails clearly if none exists.
Step 2: Package the project
bash
TS=$(date +%Y%m%d-%H%M%S)
bash "<skill_dir>/scripts/build-zip.sh" "<project-root>" "./apexguru-${TS}.zip"
Output JSON gives
,
,
,
,
. The script enforces
the
200MB compressed limit and fails fast if exceeded. On error (
/
fields), relay the hint and stop.
Step 3: Submit and poll
bash
bash "<skill_dir>/scripts/run-scan.sh" "./apexguru-${TS}.zip" "./apexguru-raw-${TS}.json"
- Add if the user wants a quicker/cheaper run (skips LLM-heavy fix
generation).
- The endpoint follows the token's environment — the base URL is derived
from the token's claim: a prod org hits , and an
internal stage/dev org hits /. Customers
authenticate a prod org, so they always hit prod; no extra flags or config.
- picks which authenticated org the JWT is derived from
(omit to use the CLI's default org).
- Progress (
QUEUED → RUNNING → SUCCEEDED
) streams to stderr; the script polls
~every 15s. Default ceiling is 10 min (, to adjust).
- On success, stdout is a one-line JSON summary and the full raw body is written to
. On failure, stdout is
{error, httpStatus, status, hint}
—
relay the hint. For status-code specifics see <skill_dir>/references/error-handling.md
.
- Foreground only. Do not background this; polling output must be observed.
- A SUCCEEDED scan is not the finish line. The raw result is a base64 blob,
not a user-facing answer. Do not stop or report "done" after the scan
succeeds — you MUST continue to Step 4 to decode and present the report.
Ending the turn at Step 3 leaves the user with nothing readable.
Step 4: Decode and present
bash
node "<skill_dir>/scripts/decode-report.js" "./apexguru-raw-${TS}.json" --present
is the default way to decode for presentation: it implies
(no silent caps) and prints ready-to-show markdown directly — a severity
legend (Minor / Major / Critical, plus a Tip marker when
enriches severity from production metrics), one
card per
violation (message, current code, suggested fix, help-doc link) for the
non-hotspot rules — capped at
(default 10) worst-first, with the cap
stated in the heading — and a closing
table listing
every
violation regardless of the card cap.
(a per-method
CPU-hotspot ranking from
mode, not a line-level antipattern) is
collapsed into its own ranked "CPU Hotspots" table instead of repeating a
near-identical card per method. Print this output to the user verbatim —
present immediately — do not pause to ask, and do not re-summarize it into
a shorter table.
For Step 5 drill-downs (filtering/grouping an existing result), the bare
(non-
) JSON form is fine — see the reading rules below, which apply
whenever you run the script without
.
DO NOT: invent script code, use bare
paths, decode base64
inline,
the
field, or Read the raw file directly.
Instructions for reading bare (non-) output
The command prints one JSON object to stdout. Read it field by field before
presenting anything — do not eyeball a partial view as complete:
- Check first, before anything else. If , was
capped to the top (default 10) rules, each group's was capped
to 3 items, and was capped to items. Never present a
result as the full picture. Re-run the same command with
appended and use that output instead. Only skip this if the user
explicitly asked for a quick/partial look.
- State attribution from / — /"Static
only" or /"Production insights". This is mandatory on every response,
per "Attribution is mandatory" above.
- is the raw API's internal rule-code tally
(e.g. , ) — it's a sanity-check total (sums to
), not a display name. Never show these codes to the user;
use the human-readable names instead (e.g.
,
SchemaGetGlobalDescribeNotEfficient
).
- (top-level) is the severity distribution across ALL
violations — use it for the summary table. Each entry has its own
scoped to just that rule.
- Build the "Violations by Rule" table from , one row per entry:
→ Rule, → Count, → Severity, and one
(or when ) → Example ().
- Build the "Top Issues" table from — already sorted
worst-severity-first. Use , , , and the first
entry of (if non-empty) as Suggested Fix. If is empty, omit
that column's value rather than inventing a fix.
- When the user asks to explain a specific violation ("what does this
mean", "why is this flagged"), surface that violation's (plain-
language why) and (Help Doc URL) verbatim — both exist on
every violation object but are intentionally left out of the summary tables
in step 5/6 to keep those scannable. Fall back to
references/violation-catalog.md
only if is empty.
- being is expected, not an error — the API's
field (fix code) isn't populated for every rule (notably ,
a CPU ranking with no single-line fix); don't say "no fix available", just
omit the column.
- With , each group also carries an array (every violation for
that rule, not just the 3-item ) — use instead of
when the user wants the complete list for one rule ("show me all the SOQL
unused-fields ones").
Presentation template (fallback — only when NOT using )
(the default, per Step 4 above) already renders the full
severity-legend + issue-cards + summary-table output described in the
"Instructions for reading bare output" section — just print its stdout
verbatim. Only build a table by hand from bare JSON if
genuinely
can't be used (e.g. scripting/CI context with no markdown renderer):
Filling the <Static only | Production insights>
title placeholder: derive
the label from the
field (not
alone) — it already
encodes the three states:
- "Production insights" ( with runtime metrics) —
enriched with production runtime metrics.
- "Static only" + (no runtime metrics) — org is
onboarded, but there's no runtime data for this code yet; generate a runtime
report in Scale Center.
- "Static only" + — source-only. Onboard the org to
ApexGuru for production insights.
The fenced block below is the literal rendered output — substitute the real
values and print it; do not emit any of the guidance above:
text
## ApexGuru Scan Complete — <Static only | Production insights>
**Found X performance violations** across Y files.
| Severity | Count |
|----------|-------|
| Critical (1) | X |
| High (2) | X |
| Moderate (3) | X |
### Violations by Rule
| Rule | Count | Severity | Example |
|------|-------|----------|---------|
| SOQL_IN_LOOP | 15 | High (2) | AccountService.cls:42 |
| DML_IN_LOOP | 8 | Critical (1) | AccountService.cls:60 |
| GGD | 2 | Moderate (3) | Utils.cls:12 |
### Top Issues
| # | Rule | Sev | File:Line | Suggested Fix |
|---|------|-----|-----------|---------------|
| 1 | DML_IN_LOOP | 1 | AccountService.cls:60 | Collect records; DML once after the loop |
| ... up to 10 |
Raw result: `./apexguru-raw-<TS>.json`
Scale to result size:
0 → "no performance antipatterns found";
1–10 → one
table;
11+ → severity counts + by-rule table + top 10. End with the raw result
path. Do
not append your own follow-up offer (no "I can drill in without
re-scanning…", no "filter by rule / group by file / explain a violation" menu) —
already prints the script's "show all" footer; that is the complete,
approved closing line and adding your own makes the output non-deterministic.
Rule-catalog details:
<skill_dir>/references/violation-catalog.md
.
Step 5: Drill into results (no re-scan)
Re-run
against the
same raw file with flags:
| User says | Flags |
|---|
| "show only SOQL-in-loop" | --rule SOQL_IN_LOOP --full
|
| "just the critical ones" | |
| "what's in AccountService.cls?" | --file AccountService.cls --full
|
| "group by file" / "which file is worst?" | |
| "break down by severity" | |
| "show me everything" | (or for bare JSON) |
Constraints & Gotchas
| Item | Why / Fix |
|---|
| Run scripts with absolute path | resolves against the user's CWD, not the skill dir |
| Any project layout is fine | The API walks the whole archive for Apex; collects every / under the root, no required |
| Never decode inline | It is a large base64 blob — always use |
| Use for the initial decode | Implies (no silent caps) and renders ready-to-show markdown directly — severity legend, per-issue cards, closing summary table — mirroring the reference MCP tool's presentation density |
| Never re-scan to filter | Step 5 re-decodes the existing raw file instantly |
| Attribution is pre-rendered | already prints the mode line ("Static only" / "Production insights") — print it as the exact output; never author your own attribution sentence or name the org |
| when expected | Org not onboarded to ApexGuru — tell the user, don't treat as an error |
| 401 / 403 / 404 / 400 | Token / org-ownership / scanId / zip issues — see references/error-handling.md |
| Foreground only, ~15s polls | Backgrounding loses progress; scans can take minutes |
| Token is a secret | never echoes it; don't print it or write it to result files |
| Not a security/lint scanner | For PMD/ESLint/security, use |
Reference & Script Index
Scripts (execute via
/
with the absolute
prefix, never Read):
| File | When to use |
|---|
<skill_dir>/scripts/resolve-token.sh
| Resolve SFAP JWT + base URL (called by run-scan.sh) |
<skill_dir>/scripts/validate-token.js
| Local (no-network) JWT pre-flight: env/scope/expiry (called by resolve-token.sh) |
<skill_dir>/scripts/build-zip.sh
| Step 2 — collect the project's Apex into a size-checked zip |
<skill_dir>/scripts/run-scan.sh
| Step 3 — submit + poll to completion |
<skill_dir>/scripts/decode-report.js
| Steps 4–5 — decode base64 report, group/filter violations |
References (read on demand):
| File | When to read |
|---|
references/authentication.md
| Where the SFAP JWT comes from; env-var/file setup |
references/api-reference.md
| Endpoint contracts, request/response shapes, limits |
references/violation-catalog.md
| ApexGuru rule meanings and typical fixes |
references/error-handling.md
| 400/401/403/404, FAILED, timeout, static-vs-full diagnosis |
contains a sample SUCCEEDED response and a decoded-summary sample.