Salesforce Reports
Manage Salesforce Reports (the analytics object, not deploy/package "report" status
commands) from the CLI. There is no native sf report create command — report CRUD lives
in the Analytics REST API (/services/data/vXX.0/analytics/reports). This skill wraps
that API through sf api request rest, which is the only path that authenticates correctly
with the CLI's (masked) access token.
Everything routes through one script: scripts/sfreport.sh.
Quick start
S=~/.claude/skills/salesforce-reports/scripts/sfreport.sh
"$S" setup --org org62 # installs sf CLI if missing, saves config, checks auth
"$S" list # all reports you can see
"$S" types # report types (the 'type' you pass to create)
"$S" get <id> --describe # a report's metadata (filters, columns, format)
"$S" run <id> # run synchronously, return rows
"$S" clone --from <id> --name "Copy of X" # most reliable way to create a report
"$S" create --type <apiName> --name "New" \
--column <api> --filter '<col> <op> <value>' --group <api> # report with columns/filters
"$S" delete <id> --yes # delete (HTTP 204 on success)
"$S" --help # full reference
The script picks its target org from: --org flag → saved config (setup) → the sf CLI's
configured target-org.
Setup
sfreport.sh setup is the front door:
- sf CLI presence. If
sfis missing, setup offers to install it (npm → Homebrew → yarn, whichever is present). Non-interactively it installs without prompting. - Default org + API version. Saved to
~/.config/claude-skills/salesforce-reports.envso you don't repeat--orgevery call. API version defaults to62.0(Analytics REST is stable there). - Auth check. Confirms the Analytics REST endpoint is reachable; on failure it prints
the
sf org login webcommand to run. - Global Company filter (GAM roles only — optional). See below.
Non-interactive form for scripts/CI:
"$S" setup --org myorg --api 62.0 --check-only # check, don't write config
"$S" setup --org myorg --global-company "NTT, Inc." --gc-column Account.Global_Company__c
Global Company filter (GAM roles)
A Global Account Manager scopes reports to a single global parent account. This skill can re-point that filter on any clone so you don't hand-edit metadata:
"$S" clone --from <id> --name "NTT Pipeline" \
--gc "NTT, Inc." --gc-column Account.Global_Company__c
Configure a default once at setup (GLOBAL_COMPANY + GC_COLUMN_DEFAULT) and every clone
inherits it; override per-call with --gc / --gc-column. Resolution order for the value:
--gc arg → $GLOBAL_COMPANY (env or saved config).
This is GAM-specific. Any other role can ignore it entirely — leave both blank at setup and the skill never touches report filters. To discover the right column on a report:
"$S" get <id> --describe | python3 -c \
'import sys,json;[print(f["column"]) for f in json.load(sys.stdin)["reportMetadata"]["reportFilters"]]'
How creation actually works
Two create paths, in order of reliability:
Clone (recommended).
POST /analytics/reports?cloneId=<id>with the source report's fullreportMetadata(renamed). The skill fetches/describe, renames, optionally re-points the Global Company filter, and POSTs. This always yields a working report with columns and groupings already in place.Create with metadata.
POST /analytics/reportswith areportMetadatanaming areportType. Usetypesto find a validreportTypeapi name, andget <existing> --describeto discover that type's valid column / filter / grouping api names. The skill accepts:--column <api>(repeatable) →detailColumns--filter '<col> <operator> <value>'(repeatable) →reportFilters; the value is the remainder of the string, so spaces/commas are fine (--filter 'StageName equals Closed Won'). Operators:equals,notEqual,lessThan,greaterThan,contains,startsWith, etc.--group <api>(repeatable) →groupingsDown(required forSUMMARY/MATRIX)--boolean-filter '<expr>'→reportBooleanFilter(e.g.'1 AND (2 OR 3)')--gc <value> --gc-column <api>→ adds the GAM Global Company equals-filter With no--column, create still produces an empty stub. Clone remains the easiest path when you want to start from an existing report's full layout.
"$S" create --type Opportunity --name "My Q3 Pipeline" --format SUMMARY \ --column StageName --column Amount --column CloseDate \ --filter 'StageName equals Closed Won' --filter 'Amount greaterThan 50000' \ --group StageName
Known gotchas
- Clone still needs a metadata body.
POST ...?cloneId=<id>with a bare{}returnsBAD_REQUEST: "there is no metadata". You must POST the fullreportMetadata. The skill handles this by fetching/describefirst. sf api request restDELETE is finicky. A plain-X DELETEerrorsNo 'mode' found in 'body' entry;-b ''and--body '{...}'don't help; an object-shapedheadererrorskeyValPair.map is not a function. The working form (used internally) is-f <envelope.json>withheaderas an array of"k:v"strings andbody: {"mode":"raw","raw":""}.- Don't curl with the CLI token.
sf org display [--verbose]returns a masked 54-char access token; curl with it givesINVALID_AUTH_HEADER. Always go throughsf api request rest, which authenticates internally. - API version. Defaults to
62.0. The CLI's default data API may be higher (e.g. 67.0) but Analytics REST report CRUD is verified on 62.0; bump with--apionly if you've tested.
Anti-patterns
sf data delete record --sobject Report— fails withINSUFFICIENT_ACCESS_OR_READONLYeven when the Analytics DELETE succeeds; the SObject path enforces different rights. Delete reports via the Analytics endpoint (sfreport.sh delete).- Hardcoding an org alias. The skill is org-agnostic; pass
--orgor save one viasetup. Don't bakeorg62into callers. - Treating
sf commands | grep reporthits as report CRUD. Those are package/deploy status commands (package version report,project deploy report). Unrelated. - Bare-create then expecting data. A
createwithout columns is empty by design — clone when you want a usable report.
Validated patterns
- Clone → verify returned 18-char Id → use/delete. Verified on org62: create HTTP 200,
delete HTTP 204, post-delete
get→NOT_FOUND. setup --check-onlyas a CI/pre-flight gate before any report automation.- GAM scoping by re-pointing one
reportFiltersentry on clone, rather than rebuilding the report — preserves columns, groupings, and report type.
Self-improvement
This skill ships with a lightweight feedback loop (learnings.md). Adopt or ignore — the
skill works without it.
Trigger a review when:
- The user corrects a created/cloned report's filter, name, or scope (strongest signal — log immediately; 2–3 corrections on the same theme → promote to the body).
- The sf CLI changes its body/auth handling (e.g. the DELETE envelope bug gets fixed — the
case-delete-envelope.shgolden case is the canary). - A new Analytics REST behavior or error code surfaces.
learnings.mdcrosses ~100 bullets (consolidation time).- The skill mis-triggers or fails to trigger.
Consolidation pass (5–10 min, weekly or threshold-driven): each learnings.md entry gets one
fate — apply (merge into Known gotchas / Anti-patterns / Validated patterns), capture
(leave in the log), or dismiss (delete). Bump last-consolidated: in frontmatter. Golden
cases that previously passed and now fail are the loudest signal to consolidate early.
Golden cases live in golden/ — run ORG=<sandbox> golden/run-all.sh before changing the
script.