platform-custom-report-type-generate
Use to create, generate, or validate Salesforce Custom Report Type metadata. Trigger on custom report types, CRTs, cross-object reports, report builder data sources, exposing fields for reporting across related objects, inner vs outer joins in reports, report type categories, or deployment errors for .reportType-meta.xml files. Do NOT trigger for running, editing, or filtering existing reports; creating report folders, dashboards, or list views; or non-authoring reporting questions.
Savant verdict: Analyzed
Scanned or evaluated by Savant; not yet both passing.
Live evaluation
Not evaluated yet. Workspaces can request a live evaluation.
Safety (NVIDIA SkillSpector)
Risk score
0/100
Recommendation
CAUTION
Severity
LOW
Savant decision
Passed (low risk)
SkillSpector rated it CAUTION, but its risk score is below 20/100, so Savant's policy passes it. Its findings are still listed for review. SkillSpector 2.12.0, static analysis.
0 patterns found
Structure
- No license declaredConfirm you may reuse this skill before importing it into your repository.
SKILL.md
---
name: platform-custom-report-type-generate
description: "Use to create, generate, or validate Salesforce Custom Report Type metadata. Trigger on custom report types, CRTs, cross-object reports, report builder data sources, exposing fields for reporting across related objects, inner vs outer joins in reports, report type categories, or deployment errors for .reportType-meta.xml files. Do NOT trigger for running, editing, or filtering existing reports; creating report folders, dashboards, or list views; or non-authoring reporting questions."
metadata:
version: "1.0"
domains: ["Platform"]
minApiVersion: "51.0"
---
## Specification
# Salesforce Custom Report Type Metadata Knowledge
## Overview
Custom Report Types (CRTs) define the **data framework** for Salesforce reports. They specify a primary object, up to 3 related objects, the relationship (join) between them, and which fields are available in the report builder.
## Purpose
- Enable reporting across custom objects and custom relationships not covered by standard report types
- Curate a focused set of fields for report builders (including fields reached via lookup)
- Control inner/outer join behavior to include or exclude primary records without related records
## Configuration
**File extension:** `.reportType-meta.xml`. The file basename is the report type's developer name (e.g. `AccountsWithProjects.reportType-meta.xml`). Each CRT is a single file, not nested under an object folder.
### Key Elements
Top-level `<ReportType>` children:
| Element | Required | Notes |
|---------|----------|-------|
| `<fullName>` | Yes | API identifier; must match the file name. Letters, numbers, underscores; must begin with a letter; no spaces; no trailing underscore; no consecutive underscores |
| `<label>` | Yes | Human-friendly name shown in the report type picker |
| `<description>` | Recommended | State the business "why" — who uses this and what they learn |
| `<baseObject>` | Yes | API name of the primary object (e.g. `Account`, `Project__c`). Cannot be changed after initial creation. All objects, including custom and external, are supported (external objects from API 38.0+) |
| `<category>` | Recommended | Report builder category — see `references/category-values.md` |
| `<deployed>` | Yes | `true` to expose to users; `false` while building/iterating |
| `<join>` | Conditional | Adds a related object and its join behavior. Nest further `<join>` blocks for deeper relationships |
| `<sections>` | Recommended | Groups of columns available to the report type. Though not strictly required, a report without columns isn't useful |
`<sections>` (group of columns) sub-elements:
| Element | Required | Notes |
|---------|----------|-------|
| `<masterLabel>` | Yes | Section heading shown in the report builder |
| `<columns>` | Conditional | One per field exposed in the section |
`<columns>` (single field) sub-elements:
| Element | Required | Notes |
|---------|----------|-------|
| `<field>` | Yes | Field API name (or dotted lookup-traversal path) |
| `<table>` | Yes | The object the field belongs to — base object name or dotted relationship path |
| `<checkedByDefault>` | Yes | `true` if the column is selected by default in the report builder |
| `<displayNameOverride>` | No | Custom column label shown in the report builder, overriding the field's default label |
## Critical Rules (Read First)
### Rule 1: If `<fullName>` Is Present, It Must Match the File Name
In source format, `fullName` is inherited from `Metadata` and derived from the file name, so the `<fullName>` element is technically optional. The repo convention is to include it. **If you include `<fullName>`, its value must equal the file name (everything before `.reportType-meta.xml`) exactly — same characters, same casing, same underscores.**
**Wrong** — file name and `<fullName>` differ:
- File: `account_projects.reportType-meta.xml`
- `<fullName>AccountProjects</fullName>`
(Mismatch: file uses `account_projects`, fullName uses `AccountProjects`)
**Right** — file name and `<fullName>` are identical:
- File: `AccountProjects.reportType-meta.xml`
- `<fullName>AccountProjects</fullName>`
### Rule 2: Join Semantics — `outerJoin` Controls Inclusion
Each `<join>` block has an `<outerJoin>` element that determines which primary records appear in the report:
| `<outerJoin>` value | Behavior | Report Builder Label |
|---------------------|----------|----------------------|
| `false` | Inner join — only primary records that HAVE at least one related record | "Each 'A' record must have at least one related 'B' record" |
| `true` | Outer join — all primary records, with or without related records | "'A' records may or may not have related 'B' records" |
**Default when unspecified:** Use `true` (outer join) when the user wants to see all primary records regardless of children. Use `false` when the report only makes sense if children exist.
### Rule 3: Each Object Needs Its Own `<sections>` Block
Every object in the CRT (primary + each joined object) must have a corresponding `<sections>` block that lists the fields exposed for reporting. Without a section for an object, none of its fields appear in the report builder.
- `<masterLabel>` on each section is the section heading in the report builder
- `<columns>` entries list the fields — each with a `<field>` (API name) and `<table>` (object API name)
- For fields reached via lookup, use the relationship path in `<field>` (e.g. `Owner.Name` with `<table>` set to the owning object)
### Rule 4: Field API Names, Not Labels
Use exact API names for fields: standard fields use their defined names (`Name`, `CreatedDate`, `OwnerId`), custom fields use `Field__c`. Custom objects must include `__c`.
**Wrong:**
- `<field>Account Name</field>`
**Right:**
- `<field>Name</field>` with `<table>Account</table>`
### Rule 5: Relationship Path for Joined Objects
When adding a `<join>`, the `<relationship>` element must use the **child relationship name** as defined on the lookup/master-detail field pointing from the child object to the parent. For custom relationships, this typically ends in `__r`.
**Wrong:**
- `<relationship>Project</relationship>` (for a custom child relationship)
**Right:**
- `<relationship>Projects__r</relationship>` (child relationship name)
- `<relationship>Contacts</relationship>` (standard, non-custom child relationship)
### Rule 6: Maximum 4 Objects Total in a Join Chain
A single CRT can join a maximum of **four objects total** (the base object + up to 3 additional objects via nested `<join>` blocks).
### Rule 7: No Inner Join After an Outer Join
Once the join chain contains an outer join (`<outerJoin>true</outerJoin>`), every subsequent nested join must also be an outer join. An inner join that follows an outer join earlier in the sequence is not allowed.
**Wrong:**
```xml
<join>
<outerJoin>true</outerJoin> <!-- outer join first -->
<relationship>Contacts</relationship>
<join>
<outerJoin>false</outerJoin> <!-- WRONG: inner join after outer -->
<relationship>Assets</relationship>
</join>
</join>
```
**Right:**
```xml
<join>
<outerJoin>true</outerJoin>
<relationship>Contacts</relationship>
<join>
<outerJoin>true</outerJoin> <!-- outer stays outer -->
<relationship>Assets</relationship>
</join>
</join>
```
### Rule 8: `<table>` for Joined Objects Uses Dotted Path
In `<sections>`, the `<table>` element identifies which object in the join chain each column belongs to. For the base object, use the object name directly (e.g. `Account`). For joined objects, use the **dotted relationship path** from the base object.
| Object in chain | `<table>` value |
|-----------------|-----------------|
| Base (Account) | `Account` |
| First join (Account → Contacts) | `Account.Contacts` |
| Nested join (Account → Contacts → Assets) | `Account.Contacts.Assets` |
### Rule 9: Field Paths Can Traverse Lookups
`<field>` values may reference fields reached via lookup relationships using dot notation — for example `Owner.Email` (owner User's email) or `ReportsTo.CreatedBy.Contact.Owner.MobilePhone`. The `<table>` must still be the object that owns the starting field.
### Rule 10: Historical Trending Fields Use `_hst` Suffix
For a field with `trackTrending=true`, the API name in `<field>` and `<table>` uses the `_hst` suffix:
```xml
<columns>
<checkedByDefault>false</checkedByDefault>
<field>Field2__c_hst</field>
<table>CustomTrendedObject__c.CustomTrendedObject__c_hst</table>
</columns>
```
### Rule 11: Primary Object Cannot Be Changed After Deployment
Once deployed, the `<baseObject>` of a CRT is locked. To change the primary object, create a new CRT and retire the old one.
### Rule 12: `autogenerated` Is Reserved for Historical Trending
The `<autogenerated>` element (API 29.0+) marks CRTs that Salesforce created automatically when historical trending was enabled on an object. Do not set this manually on hand-authored CRTs.
## Generation Workflow
### Step 1: Gather Requirements
- Primary object API name (e.g. `Account`, `Project__c`)
- Related objects and the relationship between each (which has the lookup/master-detail to which)
- For each relationship: inner join (children required) or outer join (children optional)?
- Which fields to expose per object — aim for task-relevant, not the full field list
- Audience and category — where should this appear in the report builder picker?
- Whether this ships as `deployed=true` now or stays `deployed=false` during iteration
### Step 2: Examine Existing Examples
- Look in the project for in-project CRT patterns
- If existing report types have been retrieved from an org, compare against those structures
### Step 3: Write the Specification
Document before authoring:
- `fullName` and `label`
- `baseObject`
- Category and `deployed` state
- Join chain: for each related object — relationship name, outer vs inner join
- Section layout: one section per object, ordered list of fields
- Acceptance criteria: which records should appear when the report runs, which fields are available in the builder
### Step 4: Author the Metadata File
Start from the closest example in `examples/` and adapt it to the user's scenario:
- Primary object only (no joins) → `examples/AccountsWithIndustry.reportType-meta.xml`
- Outer join (primary records included even without children) → `examples/AccountsWithProjects.reportType-meta.xml`
- Nested inner join (every level requires children) → `examples/AccountProjectsWithTasks.reportType-meta.xml`
Name the file `<DeveloperName>.reportType-meta.xml`.
### Step 5: Validate
- Well-formed XML with correct namespace (`xmlns="http://soap.sforce.com/2006/04/metadata"`)
- File name (without `.reportType-meta.xml`) matches `<fullName>` when `<fullName>` is included
- `<baseObject>` is a valid API name and the object is deployed
- Every `<relationship>` uses the correct child relationship name (`__r` suffix for custom)
- Each object referenced in `<sections>` is part of the CRT (primary or joined)
- All `<field>` references exist on the parent `<table>` and use API names (not labels)
- `<category>` is a valid Salesforce category value
- `<deployed>` is `true` if users need to access the CRT immediately
## Reference File Index
| File | When to read |
|------|--------------|
| `examples/AccountsWithIndustry.reportType-meta.xml` | Step 2 / Step 4 — primary-object-only template |
| `examples/AccountsWithProjects.reportType-meta.xml` | Step 2 / Step 4 — outer-join template (primary included even without children) |
| `examples/AccountProjectsWithTasks.reportType-meta.xml` | Step 2 / Step 4 — nested inner-join template (every level requires children) |
| `references/category-values.md` | Step 3 — to choose a valid `<category>` value from the `ReportTypeCategory` enum |
| `references/errors-and-troubleshooting.md` | When fields don't appear in the report builder or join requirements conflict |
## Verification Checklist
### Universal Checks
- [ ] File extension is `.reportType-meta.xml`
- [ ] File basename satisfies the developer-name rules (begins with a letter, only letters/numbers/underscores, no spaces, no trailing underscore, no consecutive underscores)
- [ ] If `<fullName>` is included, it matches the file basename exactly (same characters, casing, and underscores)
- [ ] `<label>` is human-readable and under 40 characters
- [ ] `<description>` explains the business purpose
- [ ] `<baseObject>` uses a valid API name and that object is deployed
- [ ] `<category>` is a valid `ReportTypeCategory` enum value
- [ ] `<deployed>` is set appropriately (`true` for user access, `false` for in-progress iteration)
- [ ] `<autogenerated>` is NOT set manually (reserved for historical-trending CRTs)
### Join Checks
- [ ] Each `<join>` uses the correct child **relationship name** (not the lookup field API name)
- [ ] Custom relationships use `__r` suffix
- [ ] `<outerJoin>` is set intentionally: `true` = optional children, `false` = required children
- [ ] No inner join (`<outerJoin>false</outerJoin>`) appears after an outer join earlier in the sequence
- [ ] Total object count (base + joins, including nested) is 4 or fewer
### Section Checks
- [ ] Every object in the CRT has a corresponding `<sections>` block
- [ ] `<masterLabel>` on each section is descriptive
- [ ] Every `<columns>` has both `<field>` (API name) and `<table>` (object API name or dotted path)
- [ ] `<checkedByDefault>` is set for each column
- [ ] `<table>` for base object is the object API name (e.g. `Account`)
- [ ] `<table>` for joined objects uses the dotted relationship path (e.g. `Account.Projects__r`, `Account.Projects__r.Tasks__r`)
- [ ] Field references use API names (not labels); custom fields use `__c`
- [ ] Lookup traversal fields use dot notation (e.g. `Owner.Email`) with `<table>` set to the object owning the starting field
- [ ] Historical trending fields use `_hst` suffix in both `<field>` and `<table>` when applicable
- [ ] No duplicate fields within a section