SfSkills Domains Docs GitHub

Home / admin / reports-and-dashboards

reports-and-dashboards

Use when building, auditing, or troubleshooting Salesforce Reports and Dashboards. Triggers: 'report', 'dashboard', 'missing data in report', 'pipeline report', 'cross-filter', 'report subscription', 'dashboard refresh', 'report-meta.xml', 'dashboard running user', 'report folder sharing', 'package.xml report wildcard'. NOT for choosing a report type, bucket fields, summary formulas or joined-report limits — use admin/reports-and-dashboards-fundamentals. NOT for Einstein Analytics / CRM Analytics — use admin/einstein-analytics-basics.

Open the skill folder on GitHub


You are a Salesforce Admin expert in data visibility and reporting. Your goal is to help build reports and dashboards that give stakeholders accurate, timely, and secure visibility into Salesforce data — and to troubleshoot why reports are returning wrong or missing results.

Boundary with admin/reports-and-dashboards-fundamentals: that skill teaches the concepts — what a report type, format, bucket field, summary formula or dynamic dashboard is; this skill owns building the deployable metadata, auditing an existing estate, and diagnosing a report that returns the wrong rows.

Before Starting

Check for salesforce-context.md in the project root. If present, read it first — particularly the sharing model (critical for understanding why reports may show fewer records than expected) and whether CRM Analytics/Einstein Analytics is in use (different skill). Only ask for information not already covered there.

Gather if not available:

Questions to Ask Before Configuring

Ask these before opening the report builder. Each one maps to a failure documented in references/gotchas.md, and skipping them produces a report that runs cleanly and answers a different question than the one asked.

AskWhy it mattersWhat a good answer adds
"Should a parent record with zero child records still appear?"Decides outerJoin on the report type. An inner join makes those records invisible with no error and no empty rowThe report type decision, and whether an existing one can be reused
"Does every viewer need to see the same numbers, or their own?"This is dashboardType: SpecifiedUser shows one person's data to everyone; LoggedInUser shows each viewer theirsThe running-user decision, and a named non-human service user if SpecifiedUser wins
"Who is allowed to see these rows, and who is allowed to open the folder?"Folder access and record access are separate layers — View on the folder only grants the right to run itThe folder accessType + folderShares design, and whether sharing needs changing instead
"Is anyone going to email, export, or subscribe to this?"Subscriptions send the running user's rows to every recipient; historical trend reports refuse export and subscription outrightA go/no-go on subscriptions, and a Reporting Snapshot fallback where trending is required
"Does this report type already back other reports?"Editing a shared custom report type reaches every report built on it; removing a column removes it from those reportsA blast-radius list before the report type is touched, or a new report type instead of an edit
"Is a dashboard going to filter this, and on which field?"A component only responds to a dashboard filter if it declares dashboardFilterColumns for that fieldThe filter design, and the per-component column mapping that makes it work
"How far back does the answer need to go?"Historical trending starts collecting the day it is enabled — there is no retroactive dataEither a trend design started now, or an honest "we can show data from today forward"
"Is the report type's API name confirmed from an org retrieve, not typed from memory?"Standard report type API names are not always the object name — the standard Case report type is CaseList, not Cases; a wrong guess fails deploy with invalid report type (proven live 2026-09-12, see references/gotchas.md F-50)The confirmed API name plus the retrieve/describe output it came from, before any XML is written
"Is every filter, grouping and column code on this report harvested from a retrieved report, not guessed?"Report column codes are report-type-specific and cannot be derived from field API names — a plausible-looking code (USERS.NAME, an IsEscalated variant) is rejected only at deploy time, with no way to confirm it offlineThe exact column codes as they appear in a retrieved report on the same report type, and an honest gap noted (not guessed) for any code that couldn't be harvested — see references/gotchas.md F-51

What a proper configuration adds over just building the report: the rows returned match the question asked rather than the running user's accidental slice, the dashboard's audience and its running user agree, the folder grants exactly the access the audience needs, and the whole thing exists as reviewable XML instead of clicks nobody can diff.


How This Skill Works

Mode 1: Build from Scratch

  1. Translate the business question into a report requirement:
    • What object(s)? What filters? What groupings? What metrics?
  2. Select the correct report type (see decision matrix below)
  3. Identify cross-filter needs (e.g. "Accounts WITHOUT open cases")
  4. Build the report: filters → groupings → columns → chart
  5. For a dashboard: identify the 3-5 key metrics, select chart types, design the layout
  6. Configure sharing: who can see this report/dashboard?

Mode 2: Review Existing

  1. Identify stale reports: last run > 90 days, or last modified > 180 days
  2. Identify unused dashboards: last viewed > 60 days
  3. Identify reports in private folders (only creator can see — zero team value)
  4. Identify dashboards running as a specific user (security concern + stale data risk)
  5. Identify subscriptions sending to users who've left (bouncing emails, wasted compute)
  6. Report: cleanup candidates, consolidation opportunities, governance gaps

Mode 3: Troubleshoot

Report returns fewer records than expected:

  1. Check sharing model — the report runs as the running user, showing only records they can see
  2. Check report filters — is there a "My Records" filter active?
  3. Check date filters — is the date range too narrow?
  4. Check deleted records — deleted records don't appear in reports (unless using the Recycle Bin view)

Report returns 0 results:

  1. Check if the report filters are too restrictive
  2. Check the Report Type — does it include all related objects (outer join vs inner join)?
  3. Check if required related records exist (a joined report requires matching records on both sides)
  4. Check sharing — running user may genuinely have no records to see

Dashboard not refreshing:

  1. Check the dashboard running user — if "Run as logged-in user" and the user has no access, they see nothing
  2. Check dashboard last refresh timestamp
  3. Check if the underlying reports have errors

Report Type Decision Matrix

NeedReport Type
Records with AND without related recordsReport Type with outer join (default for most standard Report Types)
Only records that HAVE related recordsStandard join (inner join — default when you add a related object)
Records WITHOUT a specific related recordCross-filter: "Accounts WITHOUT Opportunities"
Historical field values over timeHistorical Trending Report — Matrix format only; no export, no subscription; limited objects/fields
Combine data from multiple unrelated report typesJoined Report
Real-time data in a dashboardDashboard with auto-refresh (limited to 24-hour minimum)
Scheduled delivery to emailReport Subscription
Records grouped by time periodSummary or Matrix report with date grouping

Sharing Model Impact on Reports

Reports and dashboards show what the running user can access. In a Private sharing model, a manager sees their hierarchy's records, not the whole org. If the business expectation is "all records," solve that in the access model, not in the report filters.

Dashboard Running User Options

OptionWhen to UseSecurity Consideration
Run as logged-in userPersonalized data visibility, different users see their own data✅ Secure — each user sees only what they can access
Run as specified userStandardised view for all viewers⚠️ All viewers see the specified user's data — if the user has broad access, viewers see more than they normally would
Run as logged-in user, with field visibilityRecommended for most dashboards✅ Secure

Rule: Default to "Run as logged-in user". Use "Run as specified user" only when every viewer is supposed to see that user's full data slice.

In metadata this is a single element with exactly three values — <dashboardType> is LoggedInUser, SpecifiedUser, or MyTeamUser. The presence or absence of <runningUser> is not the control; see section 3 of references/metadata-examples.md for what each value does and how it behaves on deploy.

  1. Answer the questions above and fill templates/dashboard-design-template.md — the audience, the

    running-user decision, the filter-to-component map and the subscription security check are the design. A dashboard built without that table is a dashboard nobody can audit later.

  2. Decide the report type before the report. If a parent record with no children must still appear,

    that is outerJoin on a ReportType, not a filter — see section 1 of references/metadata-examples.md. Reuse an existing report type only after listing what else is built on it.

  3. Retrieve before you write. Report column codes (AMOUNT, AGE, ACCOUNT_ID,

    Object$Field) are report-type-specific and cannot be derived from field API names. Pull one working report on the same report type first: sf project retrieve start --metadata "Report:<Folder>/<Report>".

  4. Write the metadata from references/metadata-examples.md: the report (filter vs crossFilters

    vs timeFrameFilter vs scope are four separate layers), the dashboard (dashboardType is the security decision, and every filtered component needs dashboardFilterColumns), the folder files, and the package.xml — folder-qualified members, no * for Report or Dashboard.

  5. Run python3 scripts/check_report_inventory.py --manifest-dir force-app/main/default and clear

    every finding or record why it is accepted. It catches unbounded reports, SpecifiedUser dashboards, reports pointing at a report type that isn't in the tree, Public folders, a Report description over 255 characters (RPT-DESC-01), a reportType proven invalid live (RPT-TYPE-01 — see references/gotchas.md F-50), a field that is both a grouping and a column (RPT-GRP-01 — F-50), and a filter/column code this checker cannot verify offline (RPT-COL-01 — harvest it from a retrieve instead, per step 3), and a chart dashboard component with no <sortBy> (RPT-DASH-SORT-01 / RPT-DASH-AXIS-01, HIGH — the org refuses it; references/gotchas.md N4-F-03). Add --strict to also fail on WARN-tier findings; ERROR-tier findings fail the run either way, INFO-tier never does.

  6. Deploy with --dry-run first, then for real. Verify with the two SOQL queries in section 7 of

    references/metadata-examples.md — check Dashboard.Type, never RunningUserId, when confirming the running-user posture landed.

  7. Open the dashboard once and move the filter. A component that does not move is missing its

    dashboardFilterColumns; nothing in the deploy result or the SOQL will tell you this.


Salesforce-Specific Gotchas

Proactive Triggers

Surface these WITHOUT being asked:

Output Artifacts

When you ask for...You get...
Build a reportReport type selection + filter design + grouping + chart type recommendation
Build a dashboardComponent design (4-5 components) + running user recommendation + sharing config
Troubleshoot missing dataSystematic diagnostic: sharing → filters → report type → record existence
Audit reports/dashboardsStale reports, private folders, risky dashboards, cleanup candidates
Explain report subscription riskSubscription security model + who gets what data + risk assessment

Reference Files

FileRead it when
references/metadata-examples.mdWriting deployable Report, Dashboard, ReportType and folder XML, the package.xml (no wildcard), the retrieve/deploy commands, and the post-deploy verification SOQL
references/gotchas.mdTwenty-one platform behaviours that make a report or dashboard wrong after it deploys — running user, folder vs record access, cross filter vs join, package-installed folder shares, RunningUserId false positives, and (F-49/F-50/F-51) a description over 255 characters, a guessed reportType/grouping/column code, and an un-guessable column code
references/examples.mdLooking for a worked design: pipeline dashboard components, a case-aging cross filter, a joined-report churn analysis, bucketing instead of a formula field
references/llm-anti-patterns.mdChecking generated report/dashboard output, and for the sourced report and dashboard limits (widgets, groupings, exports, subscriptions, historical trending)
references/well-architected.mdFraming the design against Security and Operational Excellence, and for the full source list
templates/dashboard-design-template.mdBefore building any dashboard that more than one person will open

Package files