
Sf Metadata
- 1.4k installs
- 423 repo stars
- Updated April 27, 2026
- jaganpro/sf-skills
sf-metadata is an agent skill that apply sf-metadata agent skill workflows from documented skill.md guidance.
About
sf-metadata is an agent skill from jaganpro/sf-skills that apply sf-metadata agent skill workflows from documented skill.md guidance. # sf-metadata: Salesforce Metadata Generation and Org Querying Use this skill when the user needs **metadata definition or org metadata discovery**: custom objects, fields, validation rules, record types, page layouts, permission sets, or schema inspection with `sf` CLI. ## When This Skill Owns the Task Use `sf-metadata` when the work involves: Developers invoke sf-metadata during build/integrations work for automation & workflows tasks. The skill documents triggers, prerequisites, and step-by-step workflows grounded in SKILL.md. Compatible with Claude Code, Cursor, and Codex agent runtimes that load marketplace skills. Review the Security Audits panel on this listing before installing in production environments. Category Automation & Workflows with development vertical focus supports repeatable agent-guided delivery.
- sf-metadata: Salesforce Metadata Generation and Org Querying
- When This Skill Owns the Task
- Use `sf-metadata` when the work involves:
- object, field, validation rule, record type, layout, profile, or permission-set metadata
- `.object-meta.xml`, `.field-meta.xml`, `.profile-meta.xml`, and related metadata files
Sf Metadata by the numbers
- 1,407 all-time installs (skills.sh)
- +5 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #223 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
sf-metadata capabilities & compatibility
- Capabilities
- sf metadata: salesforce metadata generation and · when this skill owns the task · use `sf metadata` when the work involves: · object, field, validation rule, record type, lay · `.object meta.xml`, `.field meta.xml`, `.profile
- Use cases
- orchestration
What sf-metadata says it does
Use `sf-metadata` when the work involves:
- object, field, validation rule, record type, layout, profile, or permission-set metadata
- `.object-meta.xml`, `.field-meta.xml`, `.profile-meta.xml`, and related metadata files
npx skills add https://github.com/jaganpro/sf-skills --skill sf-metadataAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.4k |
|---|---|
| repo stars | ★ 423 |
| Security audit | 3 / 3 scanners passed |
| Last updated | April 27, 2026 |
| Repository | jaganpro/sf-skills ↗ |
What it does
Apply sf-metadata agent skill workflows from documented SKILL.md guidance.
Who is it for?
Developers working on automation & workflows during build tasks.
Skip if: Tasks outside Automation & Workflows scope described in SKILL.md.
When should I use this skill?
Apply sf-metadata agent skill workflows from documented SKILL.md guidance.
What you get
Completed automation & workflows workflow aligned with SKILL.md steps.
- CustomField metadata XML files
By the numbers
- Templates use 5 placeholders: FIELD_API_NAME, FIELD_LABEL, DEFAULT_VALUE, DESCRIPTION, HELP_TEXT
Files
sf-metadata: Salesforce Metadata Generation and Org Querying
Use this skill when the user needs metadata definition or org metadata discovery: custom objects, fields, validation rules, record types, page layouts, permission sets, or schema inspection with sf CLI.
When This Skill Owns the Task
Use sf-metadata when the work involves:
- object, field, validation rule, record type, layout, profile, or permission-set metadata
.object-meta.xml,.field-meta.xml,.profile-meta.xml, and related metadata files- describing schema before coding or Flow work
- generating metadata XML from requirements
Delegate elsewhere when the user is:
- analyzing permission access rather than defining metadata → sf-permissions
- deploying metadata → sf-deploy
- editing Flow XML → sf-flow
---
Required Context to Gather First
Ask for or infer:
- whether the user wants generation or querying
- metadata type(s) involved
- target object / field / package directory
- target org alias if querying is required
- whether new custom objects or fields should also include permission-set / FLS generation
Unless the user explicitly opts out, assume new custom objects or fields need permission-set follow-up.
---
Recommended Workflow
1. Choose the mode
| Mode | Use when |
|---|---|
| generation | the user wants new or updated metadata XML |
| querying | the user needs object / field / metadata discovery |
2. Start from templates or CLI describe data
For generation, use the assets under:
assets/objects/assets/fields/assets/permission-sets/assets/profiles/assets/record-types/assets/validation-rules/assets/layouts/
For querying, prefer sf metadata and sobject describe commands.
Recent SDR/CLI support worth knowing when reading older examples: CnfgItemSourceDefinition, ExtlClntAppOauthSecuritySettings, and UIBundle are now source-supported under their current names. See references/metadata-types-reference.md.
3. Validate metadata quality
Check:
- naming conventions
- structural correctness
- field-type fit
- security / FLS implications
- downstream deployment dependencies
4. Plan permission impact by default
When new custom fields or objects are created:
- default to generating or updating a Permission Set unless the user opts out
- include
fieldPermissionsfor eligible custom fields - note any metadata categories that are excluded because Salesforce treats them as system-managed or always-available
- remember that object CRUD alone does not make custom fields visible
5. Hand off deployment
Use sf-deploy when the user needs the metadata rolled out.
---
High-Signal Rules
- field-level security is often the hidden blocker after deployment
- object permissions ≠ field permissions
- prefer permission sets over profile-centric access patterns
- generate Permission Set follow-up by default for new custom objects and fields
- include
fieldPermissionsfor eligible custom fields instead of leaving FLS as a manual afterthought - avoid hardcoded IDs in formulas or metadata logic
- validation rules should have intentional bypass strategy when operationally necessary
- create metadata before attempting Flow or data tasks that depend on it
---
Output Format
When finishing, report in this order: 1. Metadata created or queried 2. Files created or updated 3. Key schema/security decisions 4. Permission / layout follow-ups 5. Deploy next step
Suggested shape:
Metadata task: <generate / query>
Items: <objects, fields, rules, layouts, permsets>
Files: <paths>
Notes: <naming, field types, security, dependencies>
Next step: <deploy, assign permset, or verify in Setup>---
Cross-Skill Integration
| Need | Delegate to | Reason |
|---|---|---|
| deploy metadata | sf-deploy | rollout and validation |
| build Flows on new schema | sf-flow | declarative automation |
| build Apex on new schema | sf-apex | code against metadata |
| analyze permission access after creation | sf-permissions | access auditing |
| seed data after deploy | sf-data | test data creation |
---
Reference Map
Start here
- references/field-and-cli-reference.md
- references/metadata-types-reference.md
- references/naming-conventions.md
- references/orchestration.md
Security / scoring / examples
- references/fls-best-practices.md
- references/permset-auto-generation.md
- references/best-practices-scoring.md
- references/field-types-guide.md
- references/field-types-example.md
- references/custom-object-example.md
- references/permission-set-example.md
- references/profile-permission-guide.md
- references/sf-cli-commands.md
- assets/
---
Score Guide
| Score | Meaning |
|---|---|
| 108+ | strong production-ready metadata |
| 96–107 | good metadata with minor review items |
| 84–95 | acceptable but validate carefully |
| < 84 | block deployment until corrected |
<?xml version="1.0" encoding="UTF-8"?>
<!--
Checkbox Field Template
Usage: Boolean true/false values
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Is_Active__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Is Active")
- {{DEFAULT_VALUE}} - true or false (required for checkboxes)
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Notes:
- Checkboxes are never required (always have a value)
- Default value is mandatory
- Good for flags, toggles, opt-ins
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>Checkbox</type>
<defaultValue>{{DEFAULT_VALUE}}</defaultValue>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Currency Field Template
Usage: Monetary values (respects org currency settings)
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Total_Amount__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Total Amount")
- {{PRECISION}} - Total digits (1-18)
- {{SCALE}} - Decimal places (0-17, typically 2 for currency)
- {{REQUIRED}} - true or false
- {{DEFAULT_VALUE}} - Optional default (e.g., "0.00")
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Notes:
- Automatically displays with org's currency symbol
- In multi-currency orgs, stores in corporate currency
- Use precision=18, scale=2 for standard currency
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>Currency</type>
<precision>{{PRECISION}}</precision>
<scale>{{SCALE}}</scale>
<required>{{REQUIRED}}</required>
<defaultValue>{{DEFAULT_VALUE}}</defaultValue>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Date Field Template
Usage: Calendar dates (without time component)
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Due_Date__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Due Date")
- {{REQUIRED}} - true or false
- {{DEFAULT_VALUE}} - Optional default (use formula like TODAY())
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Notes:
- For DateTime fields, use type "DateTime" instead
- Default value can use formulas: TODAY(), TODAY() + 30
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>Date</type>
<required>{{REQUIRED}}</required>
<defaultValue>{{DEFAULT_VALUE}}</defaultValue>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Email Field Template
Usage: Email addresses with format validation
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Work_Email__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Work Email")
- {{REQUIRED}} - true or false
- {{UNIQUE}} - true or false
- {{EXTERNAL_ID}} - true or false
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Notes:
- Automatically validates email format
- Max 80 characters
- Clickable in Lightning (opens email client)
- Use for any email address field beyond standard Email
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>Email</type>
<required>{{REQUIRED}}</required>
<unique>{{UNIQUE}}</unique>
<externalId>{{EXTERNAL_ID}}</externalId>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Formula Field Template
Usage: Calculated field based on other fields
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Full_Name__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Full Name")
- {{FORMULA_RETURN_TYPE}} - Text, Number, Currency, Date, DateTime, Checkbox
- {{FORMULA}} - The formula expression
- {{PRECISION}} - Total digits (for Number/Currency/Percent)
- {{SCALE}} - Decimal places (for Number/Currency/Percent)
- {{BLANK_AS_ZERO}} - true (treat blank as zero) or false (treat as blank)
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Return Type Options:
- Text: Returns string value
- Number: Returns numeric value
- Currency: Returns monetary value
- Percent: Returns percentage (0-100 stored, displayed with %)
- Date: Returns date value
- DateTime: Returns date/time value
- Checkbox: Returns true/false
Notes:
- Formula fields are read-only
- Counted against formula character limit (5,000 compiled)
- Can reference fields up to 10 relationships away
- Use TEXT() for picklist values in formulas
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>{{FORMULA_RETURN_TYPE}}</type>
<!-- Include precision/scale for Number, Currency, Percent types -->
<precision>{{PRECISION}}</precision>
<scale>{{SCALE}}</scale>
<formula>{{FORMULA}}</formula>
<formulaTreatBlanksAs>{{BLANK_AS_ZERO}}</formulaTreatBlanksAs>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>
<!--
Common Formula Examples:
Text concatenation:
FirstName__c & " " & LastName__c
Conditional (IF):
IF(Amount__c > 100000, "Large", IF(Amount__c > 10000, "Medium", "Small"))
Date calculation:
TODAY() - CreatedDate
Picklist reference:
TEXT(Status__c)
Cross-object reference:
Account.Industry
NULL handling:
BLANKVALUE(Phone__c, "No Phone")
CASE statement:
CASE(Status__c,
"New", 1,
"In Progress", 2,
"Completed", 3,
0)
-->
<?xml version="1.0" encoding="UTF-8"?>
<!--
Lookup Field Template
Usage: Optional relationship to another object
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Primary_Contact__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Primary Contact")
- {{RELATED_OBJECT}} - Object to relate to (e.g., "Contact")
- {{RELATIONSHIP_LABEL}} - Label on related list (e.g., "Related Accounts")
- {{RELATIONSHIP_NAME}} - API name for relationship (e.g., "Related_Accounts")
- {{REQUIRED}} - true or false
- {{DELETE_CONSTRAINT}} - SetNull, Restrict, or blank
- {{LOOKUP_FILTER}} - Optional filter criteria (XML block)
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Delete Constraint Options:
- (blank): Clear the value (default)
- SetNull: Set to null when parent deleted
- Restrict: Prevent deletion if children exist
Notes:
- For required parent relationships, use Master-Detail instead
- Lookup relationships don't support roll-up summaries
- Child records are NOT deleted when parent is deleted
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>Lookup</type>
<referenceTo>{{RELATED_OBJECT}}</referenceTo>
<relationshipLabel>{{RELATIONSHIP_LABEL}}</relationshipLabel>
<relationshipName>{{RELATIONSHIP_NAME}}</relationshipName>
<required>{{REQUIRED}}</required>
<deleteConstraint>{{DELETE_CONSTRAINT}}</deleteConstraint>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
<!--
Optional: Lookup Filter
<lookupFilter>
<active>true</active>
<booleanFilter>1</booleanFilter>
<filterItems>
<field>{{RELATED_OBJECT}}.IsActive__c</field>
<operation>equals</operation>
<value>true</value>
</filterItems>
<isOptional>false</isOptional>
</lookupFilter>
-->
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Master-Detail Field Template
Usage: Required parent relationship with cascade delete
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Account__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Account")
- {{RELATED_OBJECT}} - Parent object (e.g., "Account")
- {{RELATIONSHIP_LABEL}} - Label on related list (e.g., "Line Items")
- {{RELATIONSHIP_NAME}} - API name for relationship (e.g., "Line_Items")
- {{RELATIONSHIP_ORDER}} - 0 for primary, 1 for secondary M-D
- {{REPARENTABLE}} - true or false (allow changing parent)
- {{WRITE_REQUIRES_MASTER}} - true or false
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Notes:
- Child records are deleted when parent is deleted
- Supports roll-up summary fields on parent
- Child sharing is controlled by parent (ControlledByParent)
- Cannot be converted to Lookup after creation
- Maximum 2 Master-Detail fields per object
- First M-D determines the primary parent for sharing
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>MasterDetail</type>
<referenceTo>{{RELATED_OBJECT}}</referenceTo>
<relationshipLabel>{{RELATIONSHIP_LABEL}}</relationshipLabel>
<relationshipName>{{RELATIONSHIP_NAME}}</relationshipName>
<relationshipOrder>{{RELATIONSHIP_ORDER}}</relationshipOrder>
<reparentableMasterDetail>{{REPARENTABLE}}</reparentableMasterDetail>
<writeRequiresMasterRead>{{WRITE_REQUIRES_MASTER}}</writeRequiresMasterRead>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Multi-Select Picklist Field Template
Usage: Multiple-select dropdown with predefined values
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Categories__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Categories")
- {{VISIBLE_LINES}} - Number of visible lines (3-10)
- {{REQUIRED}} - true or false
- {{RESTRICTED}} - true (only defined values) or false
- {{SORTED}} - true (alphabetical) or false (maintain order)
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Notes:
- Values stored as semicolon-separated string
- Use INCLUDES() function in formulas to check values
- Cannot be used in roll-up summaries or unique constraints
- Maximum 500 values
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>MultiselectPicklist</type>
<visibleLines>{{VISIBLE_LINES}}</visibleLines>
<required>{{REQUIRED}}</required>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
<valueSet>
<restricted>{{RESTRICTED}}</restricted>
<valueSetDefinition>
<sorted>{{SORTED}}</sorted>
<!-- Example values - replace with actual values -->
<value>
<fullName>Category_A</fullName>
<default>false</default>
<label>Category A</label>
</value>
<value>
<fullName>Category_B</fullName>
<default>false</default>
<label>Category B</label>
</value>
<value>
<fullName>Category_C</fullName>
<default>false</default>
<label>Category C</label>
</value>
</valueSetDefinition>
</valueSet>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Number Field Template
Usage: Numeric values (integers or decimals)
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Quantity__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Quantity")
- {{PRECISION}} - Total digits (1-18)
- {{SCALE}} - Decimal places (0-17, must be less than precision)
- {{REQUIRED}} - true or false
- {{UNIQUE}} - true or false
- {{EXTERNAL_ID}} - true or false
- {{DEFAULT_VALUE}} - Optional default (e.g., "0")
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Examples:
- Integer: precision=18, scale=0
- Decimal (2 places): precision=18, scale=2
- Percentage: precision=5, scale=2 (stores as decimal, displays as %)
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>Number</type>
<precision>{{PRECISION}}</precision>
<scale>{{SCALE}}</scale>
<required>{{REQUIRED}}</required>
<unique>{{UNIQUE}}</unique>
<externalId>{{EXTERNAL_ID}}</externalId>
<defaultValue>{{DEFAULT_VALUE}}</defaultValue>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Phone Field Template
Usage: Phone numbers with click-to-dial support
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Mobile_Phone__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Mobile Phone")
- {{REQUIRED}} - true or false
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Notes:
- Max 40 characters
- Supports click-to-dial in Lightning (with telephony integration)
- No automatic format validation (use validation rule if needed)
- Consider using libphonenumber for advanced validation
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>Phone</type>
<required>{{REQUIRED}}</required>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Picklist Field Template
Usage: Single-select dropdown with predefined values
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Status__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Status")
- {{REQUIRED}} - true or false
- {{RESTRICTED}} - true (only defined values) or false (allows new values)
- {{SORTED}} - true (alphabetical) or false (maintain order)
- {{PICKLIST_VALUES}} - Replace entire section with value elements
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Notes:
- For reusable picklists across objects, use Global Value Sets instead
- Restricted picklists prevent data quality issues
- Consider validation rules for complex dependencies
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>Picklist</type>
<required>{{REQUIRED}}</required>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
<valueSet>
<restricted>{{RESTRICTED}}</restricted>
<valueSetDefinition>
<sorted>{{SORTED}}</sorted>
<!-- Example values - replace with actual values -->
<value>
<fullName>New</fullName>
<default>true</default>
<label>New</label>
</value>
<value>
<fullName>In_Progress</fullName>
<default>false</default>
<label>In Progress</label>
</value>
<value>
<fullName>Completed</fullName>
<default>false</default>
<label>Completed</label>
</value>
<value>
<fullName>Cancelled</fullName>
<default>false</default>
<label>Cancelled</label>
</value>
</valueSetDefinition>
</valueSet>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Roll-Up Summary Field Template
Usage: Aggregate child records (only on Master-Detail parent)
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Total_Amount__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Total Amount")
- {{SUMMARIZED_OBJECT}} - Child object API name (e.g., "Line_Item__c")
- {{SUMMARY_TYPE}} - SUM, COUNT, MIN, or MAX
- {{SUMMARIZED_FIELD}} - Field to aggregate (for SUM, MIN, MAX)
- {{FILTER_CRITERIA}} - Optional filter (XML block)
- {{PRECISION}} - Total digits (for currency/number results)
- {{SCALE}} - Decimal places
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Summary Type Options:
- COUNT: Count of child records (no field needed)
- SUM: Sum of numeric field values
- MIN: Minimum value
- MAX: Maximum value
Notes:
- Only available on Master-Detail parent objects
- Cannot summarize formula fields (except cross-object formulas)
- Cannot summarize Long Text Area or Multi-Select Picklist
- For Lookup relationships, use DLRS or trigger-based calculation
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>Summary</type>
<summarizedField>{{SUMMARIZED_OBJECT}}.{{SUMMARIZED_FIELD}}</summarizedField>
<summaryForeignKey>{{SUMMARIZED_OBJECT}}.{{MASTER_DETAIL_FIELD}}</summaryForeignKey>
<summaryOperation>{{SUMMARY_TYPE}}</summaryOperation>
<!-- Include precision/scale for SUM, MIN, MAX on numeric fields -->
<precision>{{PRECISION}}</precision>
<scale>{{SCALE}}</scale>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
<!--
Optional: Filter criteria (only aggregate records matching criteria)
<summaryFilterItems>
<field>{{SUMMARIZED_OBJECT}}.Status__c</field>
<operation>equals</operation>
<value>Completed</value>
</summaryFilterItems>
-->
</CustomField>
<!--
Examples:
COUNT all line items:
<summaryOperation>count</summaryOperation>
(no summarizedField needed)
SUM of Amount:
<summaryOperation>sum</summaryOperation>
<summarizedField>Line_Item__c.Amount__c</summarizedField>
MAX Close Date:
<summaryOperation>max</summaryOperation>
<summarizedField>Opportunity__c.CloseDate</summarizedField>
With Filter (only Won opportunities):
<summaryFilterItems>
<field>Opportunity__c.StageName</field>
<operation>equals</operation>
<value>Closed Won</value>
</summaryFilterItems>
-->
<?xml version="1.0" encoding="UTF-8"?>
<!--
Text Field Template
Usage: Short text fields up to 255 characters
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Account_Code__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Account Code")
- {{LENGTH}} - Maximum characters (1-255)
- {{REQUIRED}} - true or false
- {{UNIQUE}} - true or false (case-insensitive by default)
- {{EXTERNAL_ID}} - true or false (for integration)
- {{DEFAULT_VALUE}} - Optional default value
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>Text</type>
<length>{{LENGTH}}</length>
<required>{{REQUIRED}}</required>
<unique>{{UNIQUE}}</unique>
<caseSensitive>false</caseSensitive>
<externalId>{{EXTERNAL_ID}}</externalId>
<defaultValue>{{DEFAULT_VALUE}}</defaultValue>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Long Text Area Field Template
Usage: Multi-line text fields for descriptions, notes, etc.
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Description__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Description")
- {{LENGTH}} - Maximum characters (up to 131,072)
- {{VISIBLE_LINES}} - Display lines in UI (2-50)
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Notes:
- Cannot be unique or external ID
- Cannot be used in filters, roll-ups, or formulas
- For rich text with formatting, use Rich Text Area instead
- Consider character limits for API and report constraints
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>LongTextArea</type>
<length>{{LENGTH}}</length>
<visibleLines>{{VISIBLE_LINES}}</visibleLines>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<!--
URL Field Template
Usage: Web addresses with clickable links
Placeholders:
- {{FIELD_API_NAME}} - API name ending in __c (e.g., "Website__c")
- {{FIELD_LABEL}} - User-friendly label (e.g., "Website")
- {{REQUIRED}} - true or false
- {{DESCRIPTION}} - Field purpose
- {{HELP_TEXT}} - Inline help for users
Notes:
- Max 255 characters
- Automatically renders as clickable link
- Should include protocol (https://)
- Opens in new tab when clicked
-->
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{FIELD_API_NAME}}</fullName>
<label>{{FIELD_LABEL}}</label>
<type>Url</type>
<required>{{REQUIRED}}</required>
<description>{{DESCRIPTION}}</description>
<inlineHelpText>{{HELP_TEXT}}</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Page Layout Template
Usage: Define field arrangement and sections for record pages
Placeholders:
- {{LAYOUT_NAME}} - Full name (Object-Layout Name, e.g., "Account-Business Account Layout")
Notes:
- Page layouts are complex XML structures
- Consider using Lightning Record Pages instead for modern UIs
- Layout determines:
- Field arrangement in sections
- Related lists
- Actions/buttons
- Canvas apps, Visualforce pages
-->
<Layout xmlns="http://soap.sforce.com/2006/04/metadata">
<!--
═══════════════════════════════════════════
LAYOUT SECTIONS
═══════════════════════════════════════════
-->
<layoutSections>
<customLabel>false</customLabel>
<detailHeading>false</detailHeading>
<editHeading>true</editHeading>
<label>Information</label>
<layoutColumns>
<layoutItems>
<behavior>Required</behavior>
<field>Name</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Type</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>OwnerId</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style>
</layoutSections>
<layoutSections>
<customLabel>true</customLabel>
<detailHeading>true</detailHeading>
<editHeading>true</editHeading>
<label>Additional Information</label>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>Description</field>
</layoutItems>
</layoutColumns>
<layoutColumns/>
<style>TwoColumnsLeftToRight</style>
</layoutSections>
<layoutSections>
<customLabel>true</customLabel>
<detailHeading>true</detailHeading>
<editHeading>true</editHeading>
<label>System Information</label>
<layoutColumns>
<layoutItems>
<behavior>Readonly</behavior>
<field>CreatedById</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Readonly</behavior>
<field>LastModifiedById</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsLeftToRight</style>
</layoutSections>
<!--
═══════════════════════════════════════════
RELATED LISTS
═══════════════════════════════════════════
-->
<relatedLists>
<fields>FULL_NAME</fields>
<fields>CONTACT.TITLE</fields>
<fields>CONTACT.EMAIL</fields>
<fields>CONTACT.PHONE1</fields>
<relatedList>RelatedContactList</relatedList>
</relatedLists>
<relatedLists>
<fields>OPPORTUNITY.NAME</fields>
<fields>OPPORTUNITY.STAGE_NAME</fields>
<fields>OPPORTUNITY.AMOUNT</fields>
<fields>OPPORTUNITY.CLOSE_DATE</fields>
<relatedList>RelatedOpportunityList</relatedList>
</relatedLists>
<!--
═══════════════════════════════════════════
QUICK ACTIONS
═══════════════════════════════════════════
-->
<quickActionList>
<quickActionListItems>
<quickActionName>FeedItem.TextPost</quickActionName>
</quickActionListItems>
<quickActionListItems>
<quickActionName>NewTask</quickActionName>
</quickActionListItems>
<quickActionListItems>
<quickActionName>NewEvent</quickActionName>
</quickActionListItems>
<quickActionListItems>
<quickActionName>LogACall</quickActionName>
</quickActionListItems>
</quickActionList>
<!--
═══════════════════════════════════════════
DISPLAY OPTIONS
═══════════════════════════════════════════
-->
<showEmailCheckbox>false</showEmailCheckbox>
<showHighlightsPanel>false</showHighlightsPanel>
<showInteractionLogPanel>false</showInteractionLogPanel>
<showRunAssignmentRulesCheckbox>false</showRunAssignmentRulesCheckbox>
<showSubmitAndAttachButton>false</showSubmitAndAttachButton>
</Layout>
<!--
LAYOUT BEHAVIOR OPTIONS
════════════════════════════════════════════
Field behaviors:
- Required: Field must have value (red bar)
- Edit: Field is editable
- Readonly: Field is read-only
- Hidden: Field not shown (rare in layout)
Section styles:
- TwoColumnsTopToBottom
- TwoColumnsLeftToRight
- OneColumn
- CustomLinks
PAGE LAYOUT VS LIGHTNING RECORD PAGE
════════════════════════════════════════════
Page Layout (Classic + Lightning):
├── Controls field arrangement
├── Controls related lists
├── Works in Classic and Lightning
└── Assigned via Profile
Lightning Record Page (Lightning only):
├── Drag-and-drop App Builder
├── Controls page structure
├── Supports components, tabs, accordions
└── More flexible than page layouts
RECOMMENDATION: Use Lightning Record Pages for modern UIs,
Page Layouts for field organization and related lists.
FILE NAMING
════════════════════════════════════════════
Location: force-app/main/default/layouts/
Format: [ObjectName]-[Layout Name].layout-meta.xml
Examples:
- Account-Account Layout.layout-meta.xml
- Account-Business Account Layout.layout-meta.xml
- Custom_Object__c-Custom Object Layout.layout-meta.xml
-->
<?xml version="1.0" encoding="UTF-8"?>
<!--
Custom Object Template
Usage: Copy and customize for new custom objects
Placeholders:
- {{OBJECT_LABEL}} - User-friendly name (e.g., "Invoice")
- {{OBJECT_PLURAL_LABEL}} - Plural form (e.g., "Invoices")
- {{OBJECT_DESCRIPTION}} - Purpose of the object
- {{NAME_FIELD_LABEL}} - Label for Name field (e.g., "Invoice Number")
- {{NAME_FIELD_TYPE}} - Text or AutoNumber
- {{AUTO_NUMBER_FORMAT}} - Format if AutoNumber (e.g., "INV-{0000}")
- {{SHARING_MODEL}} - Private, ReadWrite, Read, ControlledByParent
-->
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<!-- Object Identity -->
<label>{{OBJECT_LABEL}}</label>
<pluralLabel>{{OBJECT_PLURAL_LABEL}}</pluralLabel>
<description>{{OBJECT_DESCRIPTION}}</description>
<!-- Name Field Configuration -->
<nameField>
<label>{{NAME_FIELD_LABEL}}</label>
<type>{{NAME_FIELD_TYPE}}</type>
<!-- Include displayFormat only for AutoNumber type -->
<displayFormat>{{AUTO_NUMBER_FORMAT}}</displayFormat>
<trackHistory>false</trackHistory>
</nameField>
<!-- Deployment Configuration -->
<deploymentStatus>Deployed</deploymentStatus>
<!-- Sharing & Security -->
<sharingModel>{{SHARING_MODEL}}</sharingModel>
<!--
Sharing Model Options:
- Private: Only owner and users above in role hierarchy
- ReadWrite: All users can read and edit
- Read: All users can read, owner can edit
- ControlledByParent: For Master-Detail relationships
-->
<!-- Feature Toggles -->
<enableHistory>true</enableHistory>
<enableActivities>true</enableActivities>
<enableReports>true</enableReports>
<enableSearch>true</enableSearch>
<enableFeeds>false</enableFeeds>
<enableBulkApi>true</enableBulkApi>
<enableSharing>true</enableSharing>
<enableStreamingApi>true</enableStreamingApi>
<!-- Search Configuration -->
<searchLayouts>
<customTabListAdditionalFields>Name</customTabListAdditionalFields>
<lookupDialogsAdditionalFields>Name</lookupDialogsAdditionalFields>
<lookupPhoneDialogsAdditionalFields>Name</lookupPhoneDialogsAdditionalFields>
<searchResultsAdditionalFields>Name</searchResultsAdditionalFields>
</searchLayouts>
<!-- Compact Layout -->
<compactLayoutAssignment>SYSTEM</compactLayoutAssignment>
</CustomObject>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Permission Set Template
Usage: Granular permissions assigned to users on top of their profile
BEST PRACTICE: Use Permission Sets for feature-based access control.
Organize by feature or function, not by user role.
Placeholders:
- {{PERMISSION_SET_NAME}} - API name (e.g., "Invoice_Manager")
- {{LABEL}} - User-friendly name (e.g., "Invoice Manager")
- {{DESCRIPTION}} - Purpose of this permission set
- {{LICENSE}} - Optional: specific license required
- {{REQUIRES_ACTIVATION}} - true for session-based permissions
Naming Convention:
- Feature-based: "Invoice_Manager", "Report_Builder"
- Integration: "Integration_API_Access"
- Admin: "Admin_User_Management"
-->
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<label>{{LABEL}}</label>
<description>{{DESCRIPTION}}</description>
<hasActivationRequired>{{REQUIRES_ACTIVATION}}</hasActivationRequired>
<!-- Optional: Restrict to specific license -->
<!-- <license>{{LICENSE}}</license> -->
<!--
═══════════════════════════════════════════
OBJECT PERMISSIONS
═══════════════════════════════════════════
Grant CRUD access for objects
-->
<objectPermissions>
<allowCreate>true</allowCreate>
<allowDelete>false</allowDelete>
<allowEdit>true</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>Custom_Object__c</object>
<viewAllRecords>false</viewAllRecords>
</objectPermissions>
<!--
═══════════════════════════════════════════
FIELD PERMISSIONS (FLS)
═══════════════════════════════════════════
Grant read/edit access for specific fields
-->
<fieldPermissions>
<editable>true</editable>
<field>Custom_Object__c.Sensitive_Field__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>false</editable>
<field>Custom_Object__c.Read_Only_Field__c</field>
<readable>true</readable>
</fieldPermissions>
<!--
═══════════════════════════════════════════
TAB SETTINGS
═══════════════════════════════════════════
Available, None, or Visible
-->
<tabSettings>
<tab>Custom_Object__c</tab>
<visibility>Visible</visibility>
</tabSettings>
<!--
═══════════════════════════════════════════
RECORD TYPE ASSIGNMENTS
═══════════════════════════════════════════
Make record types visible to users with this permission set
-->
<!--
<recordTypeVisibilities>
<recordType>Custom_Object__c.Record_Type_Name</recordType>
<visible>true</visible>
</recordTypeVisibilities>
-->
<!--
═══════════════════════════════════════════
USER PERMISSIONS (SYSTEM PERMISSIONS)
═══════════════════════════════════════════
Enable specific system-level permissions
-->
<!--
<userPermissions>
<enabled>true</enabled>
<name>ApiEnabled</name>
</userPermissions>
-->
<!--
═══════════════════════════════════════════
APEX CLASS ACCESS
═══════════════════════════════════════════
Grant access to Apex classes (for Visualforce, Lightning)
-->
<!--
<classAccesses>
<apexClass>MyApexController</apexClass>
<enabled>true</enabled>
</classAccesses>
-->
<!--
═══════════════════════════════════════════
CUSTOM PERMISSIONS
═══════════════════════════════════════════
Enable custom permissions (for feature flags)
-->
<!--
<customPermissions>
<enabled>true</enabled>
<name>Feature_Flag_Name</name>
</customPermissions>
-->
</PermissionSet>
<!--
PERMISSION SET ORGANIZATION PATTERNS
════════════════════════════════════════════
1. Feature-Based (RECOMMENDED):
- "Sales_Forecasting_Access"
- "Marketing_Campaign_Manager"
- "Customer_Support_Tools"
2. Integration-Based:
- "ERP_Integration_API"
- "Marketing_Automation_Sync"
3. Object-Based:
- "Invoice_Full_Access"
- "Contact_Read_Only"
4. Environment/Session-Based:
- "Elevated_Admin_Session" (requires activation)
- "After_Hours_Access"
PERMISSION SET GROUPS:
Combine multiple permission sets into a single assignable group:
- "Sales_Rep_Standard" = Sales_Forecasting + Account_Access + Report_Viewer
-->
<?xml version="1.0" encoding="UTF-8"?>
<!--
Profile Template
Usage: User profile defining permissions and access
RECOMMENDATION: Use Permission Sets instead of Profiles for granular permissions.
Profiles should define base access; Permission Sets for feature-specific access.
Placeholders:
- {{PROFILE_NAME}} - Profile API name
- {{DESCRIPTION}} - Profile purpose
- {{USER_LICENSE}} - License type (Salesforce, Salesforce Platform, etc.)
- {{IS_CUSTOM}} - true for custom profiles, false for standard
Notes:
- Standard profiles cannot be deleted
- Profile determines record types, page layouts, and login hours
- Field permissions and object permissions are typically managed here
- Consider using Permission Set Groups for complex permission models
-->
<Profile xmlns="http://soap.sforce.com/2006/04/metadata">
<custom>{{IS_CUSTOM}}</custom>
<description>{{DESCRIPTION}}</description>
<userLicense>{{USER_LICENSE}}</userLicense>
<!--
═══════════════════════════════════════════
OBJECT PERMISSIONS
═══════════════════════════════════════════
Define CRUD access for each object
-->
<objectPermissions>
<allowCreate>true</allowCreate>
<allowDelete>false</allowDelete>
<allowEdit>true</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>Account</object>
<viewAllRecords>false</viewAllRecords>
</objectPermissions>
<!-- Add more object permissions as needed -->
<!--
<objectPermissions>
<allowCreate>true</allowCreate>
<allowDelete>false</allowDelete>
<allowEdit>true</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>Custom_Object__c</object>
<viewAllRecords>false</viewAllRecords>
</objectPermissions>
-->
<!--
═══════════════════════════════════════════
FIELD PERMISSIONS (FLS)
═══════════════════════════════════════════
Define read/edit access for specific fields
-->
<fieldPermissions>
<editable>true</editable>
<field>Account.Industry</field>
<readable>true</readable>
</fieldPermissions>
<!-- Add more field permissions as needed -->
<!--
<fieldPermissions>
<editable>false</editable>
<field>Account.Sensitive_Field__c</field>
<readable>true</readable>
</fieldPermissions>
-->
<!--
═══════════════════════════════════════════
TAB VISIBILITY
═══════════════════════════════════════════
DefaultOn, DefaultOff, or Hidden
-->
<tabVisibilities>
<tab>standard-Account</tab>
<visibility>DefaultOn</visibility>
</tabVisibilities>
<!--
═══════════════════════════════════════════
RECORD TYPE ASSIGNMENTS
═══════════════════════════════════════════
Assign available record types
-->
<!--
<recordTypeVisibilities>
<default>true</default>
<recordType>Account.Business_Account</recordType>
<visible>true</visible>
</recordTypeVisibilities>
-->
<!--
═══════════════════════════════════════════
PAGE LAYOUT ASSIGNMENTS
═══════════════════════════════════════════
Map record types to page layouts
-->
<!--
<layoutAssignments>
<layout>Account-Account Layout</layout>
</layoutAssignments>
-->
<!--
═══════════════════════════════════════════
APPLICATION VISIBILITY
═══════════════════════════════════════════
-->
<applicationVisibilities>
<application>standard__LightningSales</application>
<default>true</default>
<visible>true</visible>
</applicationVisibilities>
<!--
═══════════════════════════════════════════
USER PERMISSIONS (SYSTEM PERMISSIONS)
═══════════════════════════════════════════
Enable specific system permissions
-->
<userPermissions>
<enabled>true</enabled>
<name>ViewSetup</name>
</userPermissions>
</Profile>
<?xml version="1.0" encoding="UTF-8"?>
<!--
Record Type Template
Usage: Create record types to segment data and customize processes
Placeholders:
- {{RECORD_TYPE_API_NAME}} - API name (e.g., "Business_Account")
- {{LABEL}} - User-friendly name (e.g., "Business Account")
- {{ACTIVE}} - true or false
- {{DESCRIPTION}} - Purpose of this record type
- {{BUSINESS_PROCESS}} - Associated process (for Opportunity, Lead, Case)
Notes:
- Record types control picklist values, page layouts, and business processes
- Each record type can have different page layouts per profile
- Use sparingly - too many record types increases complexity
-->
<RecordType xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{RECORD_TYPE_API_NAME}}</fullName>
<active>{{ACTIVE}}</active>
<label>{{LABEL}}</label>
<description>{{DESCRIPTION}}</description>
<!-- Optional: Business Process (for Lead, Opportunity, Case) -->
<!-- <businessProcess>{{BUSINESS_PROCESS}}</businessProcess> -->
<!--
═══════════════════════════════════════════
PICKLIST VALUES
═══════════════════════════════════════════
Define which picklist values are available for this record type
-->
<!--
<picklistValues>
<picklist>Status__c</picklist>
<values>
<fullName>New</fullName>
<default>true</default>
</values>
<values>
<fullName>In_Progress</fullName>
<default>false</default>
</values>
<values>
<fullName>Completed</fullName>
<default>false</default>
</values>
</picklistValues>
-->
</RecordType>
<!--
RECORD TYPE USE CASES
════════════════════════════════════════════
1. Account Record Types:
- Business Account (B2B)
- Person Account (B2C)
- Partner Account
2. Opportunity Record Types:
- New Business
- Renewal
- Upsell/Cross-sell
3. Case Record Types:
- Support Case
- Billing Inquiry
- Feature Request
4. Contact Record Types:
- Customer Contact
- Partner Contact
- Internal Contact
RECORD TYPE BEST PRACTICES
════════════════════════════════════════════
✓ Use record types when you need:
- Different page layouts
- Different picklist values
- Different sales/support processes
- Different validation rules (via formula)
✓ Keep record type count manageable (3-5 per object max)
✓ Assign default record type per profile
✓ Consider using formula field for reporting instead of
multiple record types for simple categorization
✗ Don't create record types just for reporting
✗ Don't create record types for temporary states
✗ Don't create record types that users can't distinguish
CREATING RECORD TYPE VIA METADATA
════════════════════════════════════════════
File location:
force-app/main/default/objects/[ObjectName]/recordTypes/[RecordTypeName].recordType-meta.xml
Example:
force-app/main/default/objects/Account/recordTypes/Business_Account.recordType-meta.xml
Also create/update:
- Page Layout assignments (layoutAssignments in Profile)
- Picklist value assignments
- Business Process (for Lead, Opportunity, Case)
-->
<?xml version="1.0" encoding="UTF-8"?>
<!--
Validation Rule Template
Usage: Enforce data quality by preventing invalid data entry
Placeholders:
- {{RULE_API_NAME}} - API name (e.g., "Require_Close_Date_When_Closed")
- {{ACTIVE}} - true or false
- {{DESCRIPTION}} - Rule purpose and business logic
- {{ERROR_FORMULA}} - Formula that returns TRUE when invalid
- {{ERROR_FIELD}} - Field to display error on (optional)
- {{ERROR_MESSAGE}} - User-friendly error message
Notes:
- Formula returns TRUE = validation fails (error shown)
- Formula returns FALSE = validation passes (record saved)
- Keep formulas simple for maintainability
- Consider bypass mechanism for integrations/admins
-->
<ValidationRule xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>{{RULE_API_NAME}}</fullName>
<active>{{ACTIVE}}</active>
<description>{{DESCRIPTION}}</description>
<errorConditionFormula>{{ERROR_FORMULA}}</errorConditionFormula>
<!-- Optional: Display error on specific field instead of top of page -->
<errorDisplayField>{{ERROR_FIELD}}</errorDisplayField>
<errorMessage>{{ERROR_MESSAGE}}</errorMessage>
</ValidationRule>
<!--
COMMON VALIDATION PATTERNS
════════════════════════════════════════════
1. Required Field Based on Status:
───────────────────────────────────────────
AND(
ISPICKVAL(Status__c, 'Closed'),
ISBLANK(Close_Date__c)
)
Error: "Close Date is required when Status is Closed"
2. Date Must Be Future:
───────────────────────────────────────────
Due_Date__c < TODAY()
Error: "Due Date must be in the future"
3. Date Range Validation:
───────────────────────────────────────────
End_Date__c < Start_Date__c
Error: "End Date must be after Start Date"
4. Numeric Range:
───────────────────────────────────────────
OR(
Discount_Percent__c < 0,
Discount_Percent__c > 100
)
Error: "Discount must be between 0% and 100%"
5. Email Format:
───────────────────────────────────────────
NOT(REGEX(Email__c, "^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"))
Error: "Please enter a valid email address"
6. Phone Format (US):
───────────────────────────────────────────
NOT(REGEX(Phone__c, "^\\(?[0-9]{3}\\)?[-. ]?[0-9]{3}[-. ]?[0-9]{4}$"))
Error: "Phone must be in format: (XXX) XXX-XXXX"
7. Cross-Object Validation:
───────────────────────────────────────────
AND(
NOT(ISBLANK(Account.Type)),
Account.Type != 'Customer',
Amount__c > 100000
)
Error: "Opportunities over $100K require Customer account type"
8. Dependent Picklist Enforcement:
───────────────────────────────────────────
AND(
ISPICKVAL(Type__c, 'Service'),
NOT(ISPICKVAL(Sub_Type__c, 'Consulting')),
NOT(ISPICKVAL(Sub_Type__c, 'Support'))
)
Error: "Service type requires Consulting or Support sub-type"
9. Prevent Editing Closed Records:
───────────────────────────────────────────
AND(
NOT(ISNEW()),
ISPICKVAL(PRIORVALUE(Status__c), 'Closed'),
ISCHANGED(Status__c)
)
Error: "Cannot reopen closed records"
10. Bypass Pattern (Recommended):
───────────────────────────────────────────
AND(
NOT($Permission.Bypass_Validation__c),
[your validation logic here]
)
Note: Create Custom Permission "Bypass_Validation__c" for admins/integrations
VALIDATION RULE BEST PRACTICES
════════════════════════════════════════════
✓ Include bypass mechanism for system integrations
✓ Use clear, actionable error messages
✓ Display error on specific field when possible
✓ Document business requirement in description
✓ Test with bulk data operations
✓ Consider impact on integrations and data loads
✗ Don't create complex formulas (hard to maintain)
✗ Don't duplicate validation logic in triggers
✗ Don't use validation for business logic (use flows/triggers)
✗ Don't block system users from fixing data issues
-->
Credits & Acknowledgments
This skill was built upon the collective wisdom of the Salesforce metadata and schema design community. We gratefully acknowledge the following authors and resources whose ideas, patterns, and best practices have shaped this skill.
---
Authors & Contributors
Andy Fawcett
[Salesforce Lightning Platform Enterprise Architecture](https://www.amazon.com/Lightning-Platform-Enterprise-Architecture-applications/dp/1789956714)
Key contributions:
- Enterprise architecture patterns
- Schema design principles
- Metadata-driven development approaches
Beyond the Cloud
[blog.beyondthecloud.dev](https://blog.beyondthecloud.dev/)
Key contributions:
- Custom field best practices
- Object relationship patterns
- Validation rule design
Salesforce Architects
[architect.salesforce.com](https://architect.salesforce.com/)
Key contributions:
- Data model design patterns
- Security model architecture
- Multi-tenant schema considerations
August Krys
Key contributions:
- corrections that strengthened automatic Permission Set / FLS follow-up for new custom metadata
- emphasis that object permissions do not replace field-level visibility
- practical guidance for eligible-field Permission Set generation
---
Official Salesforce Resources
Documentation
- Metadata API Developer Guide: https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/
- Object Reference: https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/
- Custom Metadata Types Guide: https://developer.salesforce.com/docs/atlas.en-us.custom_metadata_types_devguide.meta/custom_metadata_types_devguide/
- Permission Sets: https://help.salesforce.com/s/articleView?id=sf.perm_sets_overview.htm
Trailhead
- Data Modeling: https://trailhead.salesforce.com/en/content/learn/modules/data_modeling
- Formulas and Validation Rules: https://trailhead.salesforce.com/en/content/learn/modules/point_click_business_logic
- Security Model: https://trailhead.salesforce.com/en/content/learn/modules/data_security
---
Frameworks & Tools
sf CLI v2
- Author: Salesforce
- Documentation: https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/
- Primary tool for metadata operations
SFDX Project Structure
- Standard: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_source_file_format.htm
- Metadata file organization patterns
---
Community Resources
Salesforce Ben
[salesforceben.com](https://www.salesforceben.com/)
- Schema design guides
- Field type selection tutorials
- Relationship best practices
Apex Hours
[apexhours.com](https://www.apexhours.com/)
- Metadata management tutorials
- Custom metadata type patterns
- Declarative development guides
Salesforce Stack Exchange
[salesforce.stackexchange.com](https://salesforce.stackexchange.com/)
- Schema design discussions
- Metadata API troubleshooting
- Best practice recommendations
---
Key Concepts Credited
Field-Level Security
The emphasis on FLS configuration after field deployment is a community-established best practice to ensure data visibility.
Naming Conventions
Object and field naming conventions follow community standards and official Salesforce recommendations for readability and maintainability.
Validation Rule Patterns
Formula-based validation patterns are derived from official documentation and refined by community implementations.
Permission Set Model
The recommendation to use Permission Sets over Profiles for field access follows Salesforce's modern security architecture guidance.
---
Special Thanks
To the Salesforce administration and architecture community for continuously sharing schema design patterns, security model best practices, and metadata management techniques.
---
If we've missed anyone whose work influenced this skill, please let us know so we can add proper attribution.
#!/usr/bin/env python3
"""
Permission Set Generator for sf-metadata plugin.
Generates Permission Set XML for custom objects, automatically including
eligible custom fields and filtering out categories Salesforce commonly
excludes from Permission Set metadata in modern API versions:
- Universally required fields
- Name fields / always-visible fields
- Master-Detail fields
Usage:
python3 generate_permission_set.py <object_directory> [--output <path>]
Example:
python3 generate_permission_set.py force-app/main/default/objects/Customer_Feedback__c
Output:
force-app/main/default/permissionsets/Customer_Feedback_Access.permissionset-meta.xml
"""
import os
import sys
import argparse
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import List, Dict, Tuple
# XML Namespace for Salesforce metadata
SF_NAMESPACE = "http://soap.sforce.com/2006/04/metadata"
NS = {"sf": SF_NAMESPACE}
def find_element(root, tag_name: str, ns: dict) -> ET.Element:
"""Find element with or without namespace prefix."""
# Try with namespace first
elem = root.find(f'.//sf:{tag_name}', ns)
if elem is not None:
return elem
# Try without namespace (default namespace)
elem = root.find(f'.//{{{SF_NAMESPACE}}}{tag_name}')
if elem is not None:
return elem
# Try plain tag
elem = root.find(f'.//{tag_name}')
return elem
def parse_field_metadata(field_path: str) -> Dict:
"""
Parse a field metadata XML file and extract relevant information.
Returns dict with:
- api_name: Field API name
- required: Whether field is required
- type: Field type (Text, Number, Picklist, etc.)
- is_formula: Whether field is a formula
- is_rollup: Whether field is a roll-up summary
- is_master_detail: Whether field is a master-detail relationship
"""
try:
tree = ET.parse(field_path)
root = tree.getroot()
# Handle namespace
ns = {"sf": SF_NAMESPACE}
# Extract field name from filename
filename = os.path.basename(field_path)
api_name = filename.replace('.field-meta.xml', '')
# Get field type
field_type_elem = find_element(root, 'type', ns)
field_type = field_type_elem.text if field_type_elem is not None else 'Unknown'
# Check if required
required_elem = find_element(root, 'required', ns)
is_required = required_elem is not None and required_elem.text and required_elem.text.lower() == 'true'
# Check for formula
formula_elem = find_element(root, 'formula', ns)
is_formula = formula_elem is not None and formula_elem.text and len(formula_elem.text.strip()) > 0
# Check for roll-up summary
is_rollup = field_type == 'Summary'
# Check for master-detail
is_master_detail = field_type == 'MasterDetail'
return {
'api_name': api_name,
'required': is_required,
'type': field_type,
'is_formula': is_formula,
'is_rollup': is_rollup,
'is_master_detail': is_master_detail,
'path': field_path
}
except ET.ParseError as e:
print(f" ⚠️ Warning: Could not parse {field_path}: {e}")
return None
except Exception as e:
print(f" ⚠️ Warning: Error processing {field_path}: {e}")
return None
def get_object_name(object_dir: str) -> str:
"""Extract object API name from directory path."""
return os.path.basename(object_dir.rstrip('/'))
def scan_fields(object_dir: str) -> List[Dict]:
"""Scan all field metadata files in an object directory."""
fields_dir = os.path.join(object_dir, 'fields')
fields = []
if not os.path.exists(fields_dir):
print(f" ℹ️ No fields directory found at {fields_dir}")
return fields
for filename in os.listdir(fields_dir):
if filename.endswith('.field-meta.xml'):
field_path = os.path.join(fields_dir, filename)
field_info = parse_field_metadata(field_path)
if field_info:
fields.append(field_info)
return fields
def filter_fields_for_permission_set(fields: List[Dict], object_name: str) -> Tuple[List[Dict], List[Dict]]:
"""
Filter fields to determine which should be included in the generated Permission Set.
Returns:
(included_fields, excluded_fields)
"""
included = []
excluded = []
for field in fields:
exclude_reason = None
# Rule 1: Universally required fields are commonly excluded from
# Permission Set metadata in modern API versions.
if field['required']:
exclude_reason = "Required field (commonly excluded from Permission Set metadata)"
# Rule 2: Master-Detail fields are commonly treated as parent-controlled.
elif field['is_master_detail']:
exclude_reason = "Master-Detail field (commonly parent-controlled / excluded)"
# Rule 3: Name field is always visible
elif field['api_name'].lower() == 'name':
exclude_reason = "Name field (always visible)"
if exclude_reason:
field['exclude_reason'] = exclude_reason
excluded.append(field)
else:
included.append(field)
return included, excluded
def generate_permission_set_xml(object_name: str, included_fields: List[Dict]) -> str:
"""Generate Permission Set XML content."""
# Create label from object name (remove __c, add spaces)
label_name = object_name.replace('__c', '').replace('_', ' ')
perm_set_name = object_name.replace('__c', '') + '_Access'
xml_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<description>Auto-generated: Grants full access to {label_name} object and eligible custom fields</description>
<hasActivationRequired>false</hasActivationRequired>
<label>{label_name} Access</label>
<!-- Object Permissions: Full CRUD access -->
<objectPermissions>
<allowCreate>true</allowCreate>
<allowDelete>true</allowDelete>
<allowEdit>true</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>{object_name}</object>
<viewAllRecords>true</viewAllRecords>
</objectPermissions>
'''
# Add field permissions
if included_fields:
xml_content += '''
<!-- Field Permissions -->
<!-- NOTE: Some field categories are excluded because Salesforce commonly omits them from Permission Set metadata -->
<!-- NOTE: Formula/Roll-Up fields have editable=false -->
'''
for field in sorted(included_fields, key=lambda x: x['api_name']):
# Formula and Roll-Up fields can only be readable
editable = "false" if (field['is_formula'] or field['is_rollup']) else "true"
xml_content += f''' <fieldPermissions>
<editable>{editable}</editable>
<field>{object_name}.{field['api_name']}</field>
<readable>true</readable>
</fieldPermissions>
'''
xml_content += '''</PermissionSet>
'''
return xml_content
def main():
parser = argparse.ArgumentParser(
description='Generate Permission Set for a custom object using eligible-field filtering'
)
parser.add_argument(
'object_dir',
help='Path to object directory (e.g., force-app/main/default/objects/MyObject__c)'
)
parser.add_argument(
'--output', '-o',
help='Output path for Permission Set file (default: auto-generated in permissionsets/)',
default=None
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Print XML to stdout instead of writing to file'
)
args = parser.parse_args()
object_dir = args.object_dir.rstrip('/')
# Validate object directory exists
if not os.path.exists(object_dir):
print(f"❌ Error: Object directory not found: {object_dir}")
sys.exit(1)
object_name = get_object_name(object_dir)
print(f"\n🔧 Generating Permission Set for: {object_name}")
print("=" * 60)
# Scan fields
print("\n📁 Scanning fields...")
fields = scan_fields(object_dir)
print(f" Found {len(fields)} custom fields")
# Filter fields
print("\n🔍 Filtering fields for Permission Set...")
included, excluded = filter_fields_for_permission_set(fields, object_name)
# Report excluded fields
if excluded:
print(f"\n⚠️ Excluded fields ({len(excluded)}):")
for field in excluded:
print(f" ❌ {field['api_name']}: {field['exclude_reason']}")
# Report included fields
if included:
print(f"\n✅ Included fields ({len(included)}):")
for field in included:
field_type = "read-only" if (field['is_formula'] or field['is_rollup']) else "read/write"
print(f" ✓ {field['api_name']} ({field_type})")
else:
print("\n⚠️ No fields to include in Permission Set")
# Generate XML
xml_content = generate_permission_set_xml(object_name, included)
if args.dry_run:
print("\n📄 Generated XML (dry-run):")
print("-" * 60)
print(xml_content)
return
# Determine output path
if args.output:
output_path = args.output
else:
# Default: create in permissionsets directory at same level as objects
base_dir = os.path.dirname(os.path.dirname(object_dir)) # Go up from objects/
perm_sets_dir = os.path.join(base_dir, 'permissionsets')
perm_set_name = object_name.replace('__c', '') + '_Access'
output_path = os.path.join(perm_sets_dir, f"{perm_set_name}.permissionset-meta.xml")
# Create directory if needed
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# Write file
with open(output_path, 'w') as f:
f.write(xml_content)
print(f"\n✅ Permission Set generated successfully!")
print(f" 📁 Location: {output_path}")
print(f"\n💡 Next steps:")
print(f" 1. Deploy: sf project deploy start --source-dir {os.path.dirname(output_path)} --target-org <alias>")
print(f" 2. Assign: sf org assign permset --name {object_name.replace('__c', '')}_Access --target-org <alias>")
if __name__ == "__main__":
main()
#!/bin/bash
#
# Run Metadata Validation Script
#
# Usage:
# ./run_validation.sh <metadata_file>
#
# Example:
# ./run_validation.sh force-app/main/default/objects/MyObject__c/MyObject__c.object-meta.xml
#
# Validates Salesforce metadata files and provides scoring feedback.
#
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
if [ -z "$1" ]; then
echo "Usage: $0 <metadata_file>"
echo ""
echo "Validates Salesforce metadata files:"
echo " - *.object-meta.xml (Custom Objects)"
echo " - *.field-meta.xml (Custom Fields)"
echo " - *.profile-meta.xml (Profiles)"
echo " - *.permissionset-meta.xml (Permission Sets)"
echo ""
echo "Example:"
echo " $0 force-app/main/default/objects/Account/Account.object-meta.xml"
exit 1
fi
FILE_PATH="$1"
if [ ! -f "$FILE_PATH" ]; then
echo "Error: File not found: $FILE_PATH"
exit 1
fi
# Run the Python validator
python3 "$SCRIPT_DIR/validate_metadata.py" "$FILE_PATH"
#!/usr/bin/env python3
"""
Salesforce Metadata Validator
Validates Salesforce metadata XML files against best practices.
120-point scoring across 6 categories:
1. Structure & Format (20 points)
- Valid XML syntax
- Correct namespace
- API version requirements
- File path structure
2. Naming Conventions (20 points)
- API names (__c suffix)
- PascalCase naming
- Meaningful labels
- Relationship naming patterns
3. Data Integrity (20 points)
- Required field defaults
- Numeric precision/scale
- Picklist definitions
- Relationship constraints
4. Security & FLS (20 points)
- Field-level security considerations
- Sensitive data patterns
- Sharing model appropriateness
5. Documentation (20 points)
- Descriptions present
- Help text for user fields
- Error messages for validation rules
6. Best Practices (20 points)
- Permission Sets over Profiles
- No hardcoded IDs
- Global Value Sets for reusable picklists
Usage:
python validate_metadata.py /path/to/metadata-file.xml
"""
import os
import re
import sys
import xml.etree.ElementTree as ET
from typing import Dict, List, Tuple, Optional
class MetadataValidator:
"""Validates Salesforce metadata XML files."""
# Salesforce metadata namespace
NAMESPACE = {'sf': 'http://soap.sforce.com/2006/04/metadata'}
# Sensitive field patterns (potential PII/security concerns)
SENSITIVE_PATTERNS = [
r'ssn|social.?security',
r'credit.?card|cc.?number',
r'password|secret|token',
r'bank.?account|routing.?number',
r'tax.?id|ein|itin',
r'driver.?license|passport',
]
# Scoring categories
CATEGORIES = {
'structure_format': {'name': 'Structure & Format', 'max': 20, 'score': 20, 'issues': []},
'naming_conventions': {'name': 'Naming Conventions', 'max': 20, 'score': 20, 'issues': []},
'data_integrity': {'name': 'Data Integrity', 'max': 20, 'score': 20, 'issues': []},
'security_fls': {'name': 'Security & FLS', 'max': 20, 'score': 20, 'issues': []},
'documentation': {'name': 'Documentation', 'max': 20, 'score': 20, 'issues': []},
'best_practices': {'name': 'Best Practices', 'max': 20, 'score': 20, 'issues': []},
}
def __init__(self, file_path: str):
"""Initialize validator with file path."""
self.file_path = file_path
self.file_name = os.path.basename(file_path)
self.tree = None
self.root = None
self.metadata_type = self._detect_metadata_type()
self.categories = {k: dict(v) for k, v in self.CATEGORIES.items()}
for cat in self.categories.values():
cat['issues'] = []
def _detect_metadata_type(self) -> str:
"""Detect metadata type from file name."""
if self.file_name.endswith('.object-meta.xml'):
return 'CustomObject'
elif self.file_name.endswith('.field-meta.xml'):
return 'CustomField'
elif self.file_name.endswith('.profile-meta.xml'):
return 'Profile'
elif self.file_name.endswith('.permissionset-meta.xml'):
return 'PermissionSet'
elif self.file_name.endswith('.validationRule-meta.xml'):
return 'ValidationRule'
elif self.file_name.endswith('.recordType-meta.xml'):
return 'RecordType'
elif self.file_name.endswith('.layout-meta.xml'):
return 'Layout'
return 'Unknown'
def _add_issue(self, category: str, severity: str, message: str, deduction: int = 0):
"""Add an issue to a category."""
if category in self.categories:
self.categories[category]['issues'].append({
'severity': severity,
'message': message
})
if deduction > 0:
self.categories[category]['score'] = max(
0, self.categories[category]['score'] - deduction
)
def _get_text(self, element, tag: str, default: str = '') -> str:
"""Get text content of a child element."""
child = element.find(f'sf:{tag}', self.NAMESPACE)
if child is not None and child.text:
return child.text.strip()
# Try without namespace
child = element.find(tag)
if child is not None and child.text:
return child.text.strip()
return default
def validate(self) -> Dict:
"""Run all validations and return results."""
# Parse XML
try:
self.tree = ET.parse(self.file_path)
self.root = self.tree.getroot()
except ET.ParseError as e:
self._add_issue('structure_format', 'CRITICAL', f'Invalid XML: {e}', 10)
return self._build_results()
except FileNotFoundError:
self._add_issue('structure_format', 'CRITICAL', f'File not found: {self.file_path}', 20)
return self._build_results()
# Run validations by metadata type
self._validate_structure()
self._validate_naming()
self._validate_data_integrity()
self._validate_security()
self._validate_documentation()
self._validate_best_practices()
return self._build_results()
def _validate_structure(self):
"""Validate XML structure and format."""
# Check namespace
root_tag = self.root.tag
if not root_tag.startswith('{http://soap.sforce.com/2006/04/metadata}'):
# Check if it has any namespace
if not root_tag.startswith('{'):
self._add_issue(
'structure_format', 'WARNING',
'Missing Salesforce metadata namespace', 5
)
# Check file path structure
if '/objects/' in self.file_path:
if self.metadata_type == 'CustomField' and '/fields/' not in self.file_path:
self._add_issue(
'structure_format', 'WARNING',
'Field files should be in objects/[ObjectName]/fields/', 3
)
if self.metadata_type == 'ValidationRule' and '/validationRules/' not in self.file_path:
self._add_issue(
'structure_format', 'WARNING',
'Validation rules should be in objects/[ObjectName]/validationRules/', 3
)
def _validate_naming(self):
"""Validate naming conventions."""
# Get API name
api_name = self._get_text(self.root, 'fullName')
label = self._get_text(self.root, 'label')
# Custom objects/fields should end with __c
if self.metadata_type in ['CustomObject', 'CustomField']:
# Check filename for __c
base_name = self.file_name.replace('.object-meta.xml', '').replace('.field-meta.xml', '')
if not base_name.endswith('__c') and not base_name.startswith('standard-'):
self._add_issue(
'naming_conventions', 'WARNING',
f'Custom metadata should have __c suffix: {base_name}', 3
)
# Check for PascalCase (allow underscores for API names)
if api_name and not re.match(r'^[A-Z][a-zA-Z0-9_]*$', api_name.replace('__c', '')):
self._add_issue(
'naming_conventions', 'INFO',
f'API name should use PascalCase: {api_name}', 2
)
# Check label is meaningful (not just API name)
if label and api_name:
api_clean = api_name.replace('__c', '').replace('_', ' ')
if label.lower() == api_clean.lower():
self._add_issue(
'naming_conventions', 'INFO',
'Label should be user-friendly, not just API name', 1
)
# Check for abbreviations in labels
abbreviation_patterns = [
(r'\bAcct\b', 'Account'),
(r'\bOpp\b', 'Opportunity'),
(r'\bCont\b', 'Contact'),
(r'\bMgr\b', 'Manager'),
(r'\bNum\b', 'Number'),
(r'\bQty\b', 'Quantity'),
]
if label:
for pattern, suggestion in abbreviation_patterns:
if re.search(pattern, label, re.IGNORECASE):
self._add_issue(
'naming_conventions', 'INFO',
f'Avoid abbreviations in labels. Consider using "{suggestion}"', 1
)
break
def _validate_data_integrity(self):
"""Validate data integrity settings."""
if self.metadata_type == 'CustomField':
field_type = self._get_text(self.root, 'type')
required = self._get_text(self.root, 'required')
default_value = self._get_text(self.root, 'defaultValue')
# Required fields should have defaults (except lookups)
if required == 'true' and not default_value:
if field_type not in ['Lookup', 'MasterDetail']:
self._add_issue(
'data_integrity', 'INFO',
'Required fields should consider having a default value', 2
)
# Number/Currency fields should have precision/scale
if field_type in ['Number', 'Currency', 'Percent']:
precision = self._get_text(self.root, 'precision')
scale = self._get_text(self.root, 'scale')
if not precision or not scale:
self._add_issue(
'data_integrity', 'WARNING',
f'{field_type} fields should specify precision and scale', 3
)
# Lookup/Master-Detail should have relationship name
if field_type in ['Lookup', 'MasterDetail']:
rel_name = self._get_text(self.root, 'relationshipName')
if not rel_name:
self._add_issue(
'data_integrity', 'WARNING',
'Relationship fields should have relationshipName', 3
)
# Check delete constraint for lookups
if field_type == 'Lookup':
delete_constraint = self._get_text(self.root, 'deleteConstraint')
if not delete_constraint:
self._add_issue(
'data_integrity', 'INFO',
'Consider setting deleteConstraint for lookup fields', 2
)
elif self.metadata_type == 'ValidationRule':
# Check for error message
error_message = self._get_text(self.root, 'errorMessage')
if not error_message:
self._add_issue(
'data_integrity', 'WARNING',
'Validation rules must have an error message', 5
)
elif len(error_message) < 10:
self._add_issue(
'data_integrity', 'INFO',
'Error messages should be descriptive', 2
)
def _validate_security(self):
"""Validate security and FLS settings."""
# Check for sensitive field patterns
api_name = self._get_text(self.root, 'fullName', self.file_name)
label = self._get_text(self.root, 'label', '')
description = self._get_text(self.root, 'description', '')
combined_text = f"{api_name} {label} {description}".lower()
for pattern in self.SENSITIVE_PATTERNS:
if re.search(pattern, combined_text, re.IGNORECASE):
self._add_issue(
'security_fls', 'CRITICAL',
f'Potential sensitive data field detected. Ensure proper FLS and encryption.', 10
)
break
# Check sharing model for objects
if self.metadata_type == 'CustomObject':
sharing_model = self._get_text(self.root, 'sharingModel')
if sharing_model == 'ReadWrite':
self._add_issue(
'security_fls', 'INFO',
'Public Read/Write sharing model - verify this is intentional', 2
)
# For Permission Sets/Profiles, check for ModifyAllData or ViewAllData
if self.metadata_type in ['Profile', 'PermissionSet']:
user_perms = self.root.findall('.//sf:userPermissions', self.NAMESPACE)
for perm in user_perms:
name = self._get_text(perm, 'name')
enabled = self._get_text(perm, 'enabled')
if enabled == 'true' and name in ['ModifyAllData', 'ViewAllData']:
self._add_issue(
'security_fls', 'WARNING',
f'{name} permission enabled - use with caution', 5
)
def _validate_documentation(self):
"""Validate documentation elements."""
description = self._get_text(self.root, 'description')
help_text = self._get_text(self.root, 'inlineHelpText')
# Check for description
if self.metadata_type in ['CustomObject', 'CustomField', 'PermissionSet']:
if not description:
self._add_issue(
'documentation', 'WARNING',
'Add a description to explain the purpose', 5
)
elif len(description) < 20:
self._add_issue(
'documentation', 'INFO',
'Description should be more detailed', 2
)
# Check for help text on user-facing fields
if self.metadata_type == 'CustomField':
field_type = self._get_text(self.root, 'type')
# User-facing fields should have help text
if field_type not in ['Formula', 'Summary'] and not help_text:
self._add_issue(
'documentation', 'INFO',
'Consider adding help text for user guidance', 2
)
# Validation rules should have good error messages
if self.metadata_type == 'ValidationRule':
error_message = self._get_text(self.root, 'errorMessage')
if error_message and not any(word in error_message.lower() for word in ['please', 'must', 'should', 'required']):
self._add_issue(
'documentation', 'INFO',
'Error messages should be user-friendly and actionable', 2
)
def _validate_best_practices(self):
"""Validate against Salesforce best practices."""
if self.metadata_type == 'Profile':
self._add_issue(
'best_practices', 'INFO',
'Consider using Permission Sets instead of Profiles for granular access', 3
)
if self.metadata_type == 'CustomField':
field_type = self._get_text(self.root, 'type')
# Check for formula fields with hardcoded IDs
if field_type == 'Formula':
formula = self._get_text(self.root, 'formula')
if formula and re.search(r'["\'][0-9a-zA-Z]{15,18}["\']', formula):
self._add_issue(
'best_practices', 'WARNING',
'Avoid hardcoded IDs in formulas - use Custom Settings or Custom Metadata', 5
)
# Picklist fields should consider Global Value Sets
if field_type == 'Picklist':
value_set = self.root.find('.//sf:valueSetDefinition', self.NAMESPACE)
if value_set is not None:
values = value_set.findall('.//sf:value', self.NAMESPACE)
if len(values) > 10:
self._add_issue(
'best_practices', 'INFO',
'Consider using a Global Value Set for reusable picklist values', 2
)
if self.metadata_type == 'ValidationRule':
# Check for bypass pattern
formula = self._get_text(self.root, 'errorConditionFormula')
if formula and '$Permission' not in formula and '$Setup' not in formula:
self._add_issue(
'best_practices', 'INFO',
'Consider adding a bypass mechanism for admin/integration users', 3
)
def _build_results(self) -> Dict:
"""Build and return validation results."""
total_score = sum(cat['score'] for cat in self.categories.values())
max_score = sum(cat['max'] for cat in self.categories.values())
# Calculate rating
percentage = (total_score / max_score) * 100 if max_score > 0 else 0
if percentage >= 90:
rating = '⭐⭐⭐⭐⭐ Excellent'
elif percentage >= 80:
rating = '⭐⭐⭐⭐ Very Good'
elif percentage >= 70:
rating = '⭐⭐⭐ Good'
elif percentage >= 60:
rating = '⭐⭐ Needs Work'
else:
rating = '⭐ Critical Issues'
return {
'file_path': self.file_path,
'metadata_type': self.metadata_type,
'overall_score': total_score,
'max_score': max_score,
'rating': rating,
'categories': {
cat_key: {
'name': cat_data['name'],
'score': cat_data['score'],
'max_score': cat_data['max'],
'issues': cat_data['issues']
}
for cat_key, cat_data in self.categories.items()
}
}
def main():
"""CLI entry point with dual-mode input support."""
import json
file_path = None
# Mode 1: Hook mode - read from stdin JSON (PostToolUse hooks)
if not sys.stdin.isatty():
try:
hook_input = json.load(sys.stdin)
tool_input = hook_input.get("tool_input", {})
file_path = tool_input.get("file_path", "")
except (json.JSONDecodeError, EOFError):
pass
# Mode 2: CLI mode - read from command-line argument
if not file_path and len(sys.argv) >= 2:
file_path = sys.argv[1]
# Validate we have a file path
if not file_path:
print("Usage: python validate_metadata.py <metadata-file.xml>")
sys.exit(1)
# Only validate metadata files
valid_extensions = [
'.object-meta.xml', '.field-meta.xml', '.permissionset-meta.xml',
'.profile-meta.xml', '.validationRule-meta.xml', '.recordType-meta.xml',
'.layout-meta.xml'
]
if not any(file_path.endswith(ext) for ext in valid_extensions):
sys.exit(0) # Silently skip non-metadata files
validator = MetadataValidator(file_path)
results = validator.validate()
# Print results
print(f"\n{'=' * 60}")
print(f"🔍 Metadata Validation: {results['metadata_type']}")
print(f"File: {os.path.basename(file_path)}")
print(f"{'=' * 60}")
print(f"\nScore: {results['overall_score']}/{results['max_score']} {results['rating']}")
print("\nCategory Breakdown:")
for cat_key, cat_data in results['categories'].items():
score = cat_data['score']
max_score = cat_data['max_score']
name = cat_data['name']
pct = int((score / max_score) * 100) if max_score > 0 else 0
print(f" ├─ {name}: {score}/{max_score} ({pct}%)")
# Print issues
all_issues = []
for cat_data in results['categories'].values():
all_issues.extend(cat_data['issues'])
if all_issues:
print("\nIssues Found:")
for issue in all_issues:
severity = issue['severity']
message = issue['message']
icon = {'CRITICAL': '🔴', 'WARNING': '🟡', 'INFO': '🔵'}.get(severity, '⚪')
print(f" {icon} [{severity}] {message}")
else:
print("\n✅ No issues found!")
print()
return 0 if results['overall_score'] >= 72 else 1
if __name__ == "__main__":
sys.exit(main())
sf-metadata
Salesforce metadata generation and org-querying skill with 120-point scoring. Build custom objects, fields, permission sets, validation rules, and related metadata with production-focused patterns.
Features
- Metadata Generation: Custom objects, fields, layouts, validation rules, permission sets
- Org Querying: Describe objects and inspect metadata via
sfCLI - 120-Point Scoring: Validation across structure, naming, security, and best practices
- Automatic FLS Follow-Up: Default to Permission Set generation for new custom objects and fields unless the user opts out
- Cross-Skill Orchestration: Works with sf-flow, sf-data, and sf-deploy
Installation
# Install as part of sf-skills
npx skills add Jaganpro/sf-skills
# Or install just this skill
npx skills add Jaganpro/sf-skills --skill sf-metadataQuick Start
1. Invoke the skill
Skill: sf-metadata
Request: "Create a custom object Invoice__c with amount and due date fields"2. Common tasks
- Generate object and field metadata
- Query metadata types in a target org
- Generate or update permission sets for newly added fields
- Prepare metadata for deployment via sf-deploy
Documentation
- SKILL.md - Full workflow and orchestration guidance
- references/field-and-cli-reference.md - Field types and CLI commands
- references/permset-auto-generation.md - Automatic permission-set / FLS generation rules
- references/best-practices-scoring.md - 120-point scoring breakdown
- references/orchestration.md - Cross-skill execution order
Related Skills
sf-flow- Build automations on top of new metadatasf-data- Load test data after deploymentsf-deploy- Validate and deploy metadata changes
License
MIT License. See the repository root LICENSE.
<!-- Parent: sf-metadata/SKILL.md -->
Best Practices & Scoring Details (120 Points)
Category Breakdown
Structure & Format (20 points):
- Valid XML syntax (-10 if invalid)
- Correct Salesforce namespace:
http://soap.sforce.com/2006/04/metadata(-5 if missing) - API version present and >= 65.0 (-5 if outdated)
- Correct file path and naming structure (-5 if wrong)
Naming Conventions (20 points):
- Custom objects/fields end with
__c(-3 each violation) - Use PascalCase for API names:
Account_Status__cnotaccount_status__c(-2 each) - Meaningful labels (no abbreviations like
Acct,Sts) (-2 each) - Relationship names follow pattern:
[ParentObject]_[ChildObjects](-3)
Data Integrity (20 points):
- Required fields have sensible defaults or validation (-5)
- Number fields have appropriate precision/scale (-3)
- Picklist values properly defined with labels (-3)
- Relationship delete constraints specified (SetNull, Restrict, Cascade) (-3)
- Formula field syntax valid (-5)
- Roll-up summaries reference correct fields (-3)
Security & FLS (20 points):
- Field-Level Security considerations documented (-5 if sensitive field exposed)
- Sensitive field types flagged (SSN patterns, Credit Card patterns) (-10)
- Object sharing model appropriate for data sensitivity (-5)
- Permission Sets preferred over Profile modifications (advisory)
Documentation (20 points):
- Description present and meaningful on objects/fields (-5 if missing)
- Help text for user-facing fields (-3 each)
- Clear error messages for validation rules (-3)
- Inline comments in complex formulas (-3)
Best Practices (20 points):
- Use Permission Sets over Profiles when possible (-3 if Profile-first)
- Avoid hardcoded Record IDs in formulas (-5 if found)
- Use Global Value Sets for reusable picklists (advisory)
- Master-Detail vs Lookup selection appropriate for use case (-3)
- Record Types have associated Page Layouts (-3)
Scoring Thresholds
| Rating | Score | Action |
|---|---|---|
| Excellent | 108+ | Production-ready |
| Very Good | 96-107 | Minor improvements |
| Good | 84-95 | Acceptable, needs work |
| Below Standard | 72-83 | Address before deploy |
| Block | <72 | CRITICAL issues |
---
Field Template Tips
Number Field: Omit Empty Defaults
Don't include <defaultValue> if it's empty or zero — Salesforce ignores it:
<!-- WRONG: Empty default is ignored, adds noise -->
<CustomField>
<fullName>Score__c</fullName>
<type>Number</type>
<precision>3</precision>
<scale>0</scale>
<defaultValue></defaultValue> <!-- Remove this! -->
</CustomField>
<!-- CORRECT: Omit defaultValue entirely if not needed -->
<CustomField>
<fullName>Score__c</fullName>
<type>Number</type>
<precision>3</precision>
<scale>0</scale>
</CustomField>
<!-- CORRECT: Include defaultValue only if you need a specific value -->
<CustomField>
<fullName>Priority__c</fullName>
<type>Number</type>
<precision>1</precision>
<scale>0</scale>
<defaultValue>3</defaultValue> <!-- Meaningful default -->
</CustomField>Standard vs Custom Object Paths
| Object Type | Path Example |
|---|---|
| Standard (Lead) | objects/Lead/fields/Lead_Score__c.field-meta.xml |
| Custom | objects/MyObject__c/fields/MyField__c.field-meta.xml |
Common Mistake: Using Lead__c (with suffix) for standard Lead object.
<!-- Parent: sf-metadata/SKILL.md -->
Custom Object Example: Invoice
This example demonstrates creating a complete custom object with fields, validation rules, and record types.
Scenario
Create an Invoice object for tracking customer invoices with:
- Auto-numbered invoice number
- Customer (Account) relationship
- Line items (child object)
- Status tracking
- Validation rules
---
Step 1: Create Custom Object
File: force-app/main/default/objects/Invoice__c/Invoice__c.object-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Invoice</label>
<pluralLabel>Invoices</pluralLabel>
<description>Tracks customer invoices and payment status</description>
<nameField>
<label>Invoice Number</label>
<type>AutoNumber</type>
<displayFormat>INV-{0000000}</displayFormat>
<trackHistory>false</trackHistory>
</nameField>
<deploymentStatus>Deployed</deploymentStatus>
<sharingModel>Private</sharingModel>
<enableHistory>true</enableHistory>
<enableActivities>true</enableActivities>
<enableReports>true</enableReports>
<enableSearch>true</enableSearch>
<enableFeeds>false</enableFeeds>
<enableBulkApi>true</enableBulkApi>
<enableSharing>true</enableSharing>
<enableStreamingApi>true</enableStreamingApi>
<searchLayouts>
<customTabListAdditionalFields>Name</customTabListAdditionalFields>
<customTabListAdditionalFields>Account__c</customTabListAdditionalFields>
<customTabListAdditionalFields>Total_Amount__c</customTabListAdditionalFields>
<customTabListAdditionalFields>Status__c</customTabListAdditionalFields>
<lookupDialogsAdditionalFields>Name</lookupDialogsAdditionalFields>
<lookupDialogsAdditionalFields>Account__c</lookupDialogsAdditionalFields>
<searchResultsAdditionalFields>Name</searchResultsAdditionalFields>
<searchResultsAdditionalFields>Account__c</searchResultsAdditionalFields>
<searchResultsAdditionalFields>Status__c</searchResultsAdditionalFields>
</searchLayouts>
<compactLayoutAssignment>SYSTEM</compactLayoutAssignment>
</CustomObject>---
Step 2: Create Custom Fields
Account Lookup
File: force-app/main/default/objects/Invoice__c/fields/Account__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Account__c</fullName>
<label>Account</label>
<type>Lookup</type>
<referenceTo>Account</referenceTo>
<relationshipLabel>Invoices</relationshipLabel>
<relationshipName>Invoices</relationshipName>
<required>true</required>
<deleteConstraint>Restrict</deleteConstraint>
<description>The customer account for this invoice</description>
<inlineHelpText>Select the customer account</inlineHelpText>
<trackHistory>true</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>Status Picklist
File: force-app/main/default/objects/Invoice__c/fields/Status__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Status__c</fullName>
<label>Status</label>
<type>Picklist</type>
<required>true</required>
<description>Current status of the invoice</description>
<inlineHelpText>Select the invoice status</inlineHelpText>
<trackHistory>true</trackHistory>
<trackTrending>false</trackTrending>
<valueSet>
<restricted>true</restricted>
<valueSetDefinition>
<sorted>false</sorted>
<value>
<fullName>Draft</fullName>
<default>true</default>
<label>Draft</label>
</value>
<value>
<fullName>Sent</fullName>
<default>false</default>
<label>Sent</label>
</value>
<value>
<fullName>Paid</fullName>
<default>false</default>
<label>Paid</label>
</value>
<value>
<fullName>Overdue</fullName>
<default>false</default>
<label>Overdue</label>
</value>
<value>
<fullName>Cancelled</fullName>
<default>false</default>
<label>Cancelled</label>
</value>
</valueSetDefinition>
</valueSet>
</CustomField>Invoice Date
File: force-app/main/default/objects/Invoice__c/fields/Invoice_Date__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Invoice_Date__c</fullName>
<label>Invoice Date</label>
<type>Date</type>
<required>true</required>
<defaultValue>TODAY()</defaultValue>
<description>Date the invoice was issued</description>
<inlineHelpText>Date the invoice was created</inlineHelpText>
<trackHistory>true</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>Due Date
File: force-app/main/default/objects/Invoice__c/fields/Due_Date__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Due_Date__c</fullName>
<label>Due Date</label>
<type>Date</type>
<required>true</required>
<defaultValue>TODAY() + 30</defaultValue>
<description>Payment due date</description>
<inlineHelpText>Date payment is due (default: 30 days from today)</inlineHelpText>
<trackHistory>true</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>Total Amount
File: force-app/main/default/objects/Invoice__c/fields/Total_Amount__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Total_Amount__c</fullName>
<label>Total Amount</label>
<type>Currency</type>
<precision>18</precision>
<scale>2</scale>
<required>false</required>
<defaultValue>0</defaultValue>
<description>Total invoice amount (calculated from line items)</description>
<inlineHelpText>Total amount including all line items</inlineHelpText>
<trackHistory>true</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>Payment Date
File: force-app/main/default/objects/Invoice__c/fields/Payment_Date__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Payment_Date__c</fullName>
<label>Payment Date</label>
<type>Date</type>
<required>false</required>
<description>Date payment was received</description>
<inlineHelpText>Leave blank until payment is received</inlineHelpText>
<trackHistory>true</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>Days Overdue (Formula)
File: force-app/main/default/objects/Invoice__c/fields/Days_Overdue__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Days_Overdue__c</fullName>
<label>Days Overdue</label>
<type>Number</type>
<precision>18</precision>
<scale>0</scale>
<formula>IF(
AND(
ISBLANK(Payment_Date__c),
Due_Date__c < TODAY(),
NOT(ISPICKVAL(Status__c, 'Cancelled'))
),
TODAY() - Due_Date__c,
0
)</formula>
<formulaTreatBlanksAs>BlankAsZero</formulaTreatBlanksAs>
<description>Number of days past due date</description>
<inlineHelpText>Calculated days overdue (0 if paid or not yet due)</inlineHelpText>
<trackHistory>false</trackHistory>
<trackTrending>false</trackTrending>
</CustomField>---
Step 3: Create Validation Rules
Require Payment Date When Paid
File: force-app/main/default/objects/Invoice__c/validationRules/Require_Payment_Date_When_Paid.validationRule-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ValidationRule xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Require_Payment_Date_When_Paid</fullName>
<active>true</active>
<description>Ensures Payment Date is filled when Status is set to Paid</description>
<errorConditionFormula>AND(
NOT($Permission.Bypass_Validation__c),
ISPICKVAL(Status__c, 'Paid'),
ISBLANK(Payment_Date__c)
)</errorConditionFormula>
<errorDisplayField>Payment_Date__c</errorDisplayField>
<errorMessage>Payment Date is required when marking an invoice as Paid.</errorMessage>
</ValidationRule>Due Date After Invoice Date
File: force-app/main/default/objects/Invoice__c/validationRules/Due_Date_After_Invoice_Date.validationRule-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<ValidationRule xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Due_Date_After_Invoice_Date</fullName>
<active>true</active>
<description>Ensures Due Date is on or after Invoice Date</description>
<errorConditionFormula>AND(
NOT($Permission.Bypass_Validation__c),
Due_Date__c < Invoice_Date__c
)</errorConditionFormula>
<errorDisplayField>Due_Date__c</errorDisplayField>
<errorMessage>Due Date must be on or after the Invoice Date.</errorMessage>
</ValidationRule>---
Step 4: Create Record Type (Optional)
File: force-app/main/default/objects/Invoice__c/recordTypes/Standard_Invoice.recordType-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<RecordType xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Standard_Invoice</fullName>
<active>true</active>
<label>Standard Invoice</label>
<description>Standard invoice for regular billing</description>
</RecordType>---
Directory Structure
force-app/main/default/objects/Invoice__c/
├── Invoice__c.object-meta.xml
├── fields/
│ ├── Account__c.field-meta.xml
│ ├── Status__c.field-meta.xml
│ ├── Invoice_Date__c.field-meta.xml
│ ├── Due_Date__c.field-meta.xml
│ ├── Total_Amount__c.field-meta.xml
│ ├── Payment_Date__c.field-meta.xml
│ └── Days_Overdue__c.field-meta.xml
├── validationRules/
│ ├── Require_Payment_Date_When_Paid.validationRule-meta.xml
│ └── Due_Date_After_Invoice_Date.validationRule-meta.xml
└── recordTypes/
└── Standard_Invoice.recordType-meta.xml---
Deployment
# Validate
sf project deploy start \
--source-dir force-app/main/default/objects/Invoice__c \
--target-org myorg \
--dry-run
# Deploy
sf project deploy start \
--source-dir force-app/main/default/objects/Invoice__c \
--target-org myorg---
Next Steps
1. Create Permission Set for Invoice access 2. Create Page Layout 3. Create related Line Item object (Master-Detail) 4. Build Flows for automation 5. Create Reports and Dashboards
<!-- Parent: sf-metadata/SKILL.md -->
Field Type Guide, Relationships & CLI Reference
Field Type Selection Guide
| Type | Salesforce | Notes |
|---|---|---|
| Text | Text / Text Area (Long/Rich) | ≤255 chars / multi-line / HTML |
| Numbers | Number / Currency | Decimals or money (org currency) |
| Boolean | Checkbox | True/False |
| Choice | Picklist / Multi-Select | Single/multiple predefined options |
| Date | Date / DateTime | With or without time |
| Contact | Email / Phone / URL | Validated formats |
| Relationship | Lookup / Master-Detail | Optional / required parent |
| Calculated | Formula / Roll-Up | Derived from fields / children |
Relationship Decision Matrix
| Scenario | Use | Reason |
|---|---|---|
| Parent optional | Lookup | Child can exist without parent |
| Parent required | Master-Detail | Cascade delete, roll-up summaries |
| Many-to-Many | Junction Object | Two Master-Detail relationships |
| Self-referential | Hierarchical Lookup | Same object (e.g., Account hierarchy) |
| Cross-object formula | Master-Detail or Formula | Access parent fields |
Common Validation Rule Patterns
| Pattern | Formula | Use |
|---|---|---|
| Conditional Required | AND(ISPICKVAL(Status,'Closed'), ISBLANK(Close_Date__c)) | Field required when condition met |
| Email Regex | NOT(REGEX(Email__c, "^[a-zA-Z0-9._-]+@...")) | Format validation |
| Future Date | Due_Date__c < TODAY() | Date constraints |
| Cross-Object | AND(Account.Type != 'Customer', Amount__c > 100000) | Related field checks |
---
sf CLI Quick Reference
Object & Field Queries
# Describe standard or custom object
sf sobject describe --sobject Account --target-org [alias] --json
# List all custom objects
sf org list metadata --metadata-type CustomObject --target-org [alias] --json
# List all custom fields on an object
sf org list metadata --metadata-type CustomField --folder Account --target-org [alias] --jsonMetadata Operations
# List all metadata types available
sf org list metadata-types --target-org [alias] --json
# Retrieve specific metadata
sf project retrieve start --metadata CustomObject:Account --target-org [alias]
# Generate package.xml from source
sf project generate manifest --source-dir force-app --name package.xmlInteractive Generation
# Generate custom object interactively
sf schema generate sobject --label "My Object"
# Generate custom field interactively
sf schema generate field --label "My Field" --object Account<!-- Parent: sf-metadata/SKILL.md -->
Field Types Example: Complete Reference
This example demonstrates all common field types with real-world usage.
---
Text Fields
Standard Text
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Product_SKU__c</fullName>
<label>Product SKU</label>
<type>Text</type>
<length>50</length>
<required>true</required>
<unique>true</unique>
<externalId>true</externalId>
<caseSensitive>false</caseSensitive>
<description>Unique product identifier for inventory</description>
<inlineHelpText>Enter the unique SKU (e.g., SKU-12345)</inlineHelpText>
</CustomField>Long Text Area
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Internal_Notes__c</fullName>
<label>Internal Notes</label>
<type>LongTextArea</type>
<length>32000</length>
<visibleLines>6</visibleLines>
<description>Internal team notes (not visible to customers)</description>
<inlineHelpText>Add any internal notes or comments</inlineHelpText>
</CustomField>Rich Text Area
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Product_Description__c</fullName>
<label>Product Description</label>
<type>Html</type>
<length>32000</length>
<visibleLines>10</visibleLines>
<description>Rich text product description for marketing</description>
<inlineHelpText>Format description with bold, lists, and images</inlineHelpText>
</CustomField>---
Numeric Fields
Integer (Whole Number)
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Quantity__c</fullName>
<label>Quantity</label>
<type>Number</type>
<precision>18</precision>
<scale>0</scale>
<required>true</required>
<defaultValue>1</defaultValue>
<description>Number of units ordered</description>
<inlineHelpText>Enter quantity (whole numbers only)</inlineHelpText>
</CustomField>Decimal
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Weight_Kg__c</fullName>
<label>Weight (kg)</label>
<type>Number</type>
<precision>10</precision>
<scale>3</scale>
<required>false</required>
<description>Product weight in kilograms</description>
<inlineHelpText>Enter weight with up to 3 decimal places</inlineHelpText>
</CustomField>Currency
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Unit_Price__c</fullName>
<label>Unit Price</label>
<type>Currency</type>
<precision>18</precision>
<scale>2</scale>
<required>true</required>
<defaultValue>0</defaultValue>
<description>Price per unit</description>
<inlineHelpText>Enter the price per unit</inlineHelpText>
</CustomField>Percent
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Discount_Percent__c</fullName>
<label>Discount %</label>
<type>Percent</type>
<precision>5</precision>
<scale>2</scale>
<required>false</required>
<defaultValue>0</defaultValue>
<description>Discount percentage applied</description>
<inlineHelpText>Enter discount as percentage (e.g., 10 for 10%)</inlineHelpText>
</CustomField>---
Date/Time Fields
Date
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Contract_Start_Date__c</fullName>
<label>Contract Start Date</label>
<type>Date</type>
<required>true</required>
<defaultValue>TODAY()</defaultValue>
<description>Date the contract becomes effective</description>
<inlineHelpText>When does this contract take effect?</inlineHelpText>
</CustomField>DateTime
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Last_Contact_DateTime__c</fullName>
<label>Last Contact Date/Time</label>
<type>DateTime</type>
<required>false</required>
<description>Last time we contacted this customer</description>
<inlineHelpText>Date and time of most recent contact</inlineHelpText>
</CustomField>---
Boolean Field
Checkbox
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Is_VIP_Customer__c</fullName>
<label>VIP Customer</label>
<type>Checkbox</type>
<defaultValue>false</defaultValue>
<description>Indicates if customer has VIP status</description>
<inlineHelpText>Check if this is a VIP customer</inlineHelpText>
</CustomField>---
Picklist Fields
Single-Select Picklist
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Priority__c</fullName>
<label>Priority</label>
<type>Picklist</type>
<required>true</required>
<description>Task priority level</description>
<inlineHelpText>Select the priority level</inlineHelpText>
<valueSet>
<restricted>true</restricted>
<valueSetDefinition>
<sorted>false</sorted>
<value>
<fullName>Low</fullName>
<default>false</default>
<label>Low</label>
</value>
<value>
<fullName>Medium</fullName>
<default>true</default>
<label>Medium</label>
</value>
<value>
<fullName>High</fullName>
<default>false</default>
<label>High</label>
</value>
<value>
<fullName>Critical</fullName>
<default>false</default>
<label>Critical</label>
</value>
</valueSetDefinition>
</valueSet>
</CustomField>Multi-Select Picklist
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Product_Categories__c</fullName>
<label>Product Categories</label>
<type>MultiselectPicklist</type>
<visibleLines>4</visibleLines>
<required>false</required>
<description>Categories this product belongs to</description>
<inlineHelpText>Select all applicable categories</inlineHelpText>
<valueSet>
<restricted>true</restricted>
<valueSetDefinition>
<sorted>true</sorted>
<value>
<fullName>Electronics</fullName>
<label>Electronics</label>
</value>
<value>
<fullName>Clothing</fullName>
<label>Clothing</label>
</value>
<value>
<fullName>Home_Garden</fullName>
<label>Home & Garden</label>
</value>
<value>
<fullName>Sports</fullName>
<label>Sports</label>
</value>
</valueSetDefinition>
</valueSet>
</CustomField>---
Relationship Fields
Lookup
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Primary_Contact__c</fullName>
<label>Primary Contact</label>
<type>Lookup</type>
<referenceTo>Contact</referenceTo>
<relationshipLabel>Primary For</relationshipLabel>
<relationshipName>Primary_For</relationshipName>
<required>false</required>
<deleteConstraint>SetNull</deleteConstraint>
<description>Main contact person for this account</description>
<inlineHelpText>Select the primary contact</inlineHelpText>
<lookupFilter>
<active>true</active>
<filterItems>
<field>Contact.AccountId</field>
<operation>equals</operation>
<valueField>$Source.AccountId</valueField>
</filterItems>
<isOptional>false</isOptional>
</lookupFilter>
</CustomField>Master-Detail
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Order__c</fullName>
<label>Order</label>
<type>MasterDetail</type>
<referenceTo>Order__c</referenceTo>
<relationshipLabel>Line Items</relationshipLabel>
<relationshipName>Line_Items</relationshipName>
<relationshipOrder>0</relationshipOrder>
<reparentableMasterDetail>false</reparentableMasterDetail>
<writeRequiresMasterRead>false</writeRequiresMasterRead>
<description>Parent order for this line item</description>
<inlineHelpText>The order this line item belongs to</inlineHelpText>
</CustomField>---
Special Fields
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Support_Email__c</fullName>
<label>Support Email</label>
<type>Email</type>
<required>false</required>
<unique>false</unique>
<description>Customer support contact email</description>
<inlineHelpText>Email address for support inquiries</inlineHelpText>
</CustomField>Phone
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Mobile_Phone__c</fullName>
<label>Mobile Phone</label>
<type>Phone</type>
<required>false</required>
<description>Mobile phone number</description>
<inlineHelpText>Enter mobile number including country code</inlineHelpText>
</CustomField>URL
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>LinkedIn_Profile__c</fullName>
<label>LinkedIn Profile</label>
<type>Url</type>
<required>false</required>
<description>LinkedIn profile URL</description>
<inlineHelpText>Enter full LinkedIn URL (https://linkedin.com/in/...)</inlineHelpText>
</CustomField>---
Formula Fields
Text Formula
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Full_Address__c</fullName>
<label>Full Address</label>
<type>Text</type>
<formula>Street__c & BR() &
City__c & ", " & State__c & " " & Postal_Code__c & BR() &
Country__c</formula>
<description>Formatted complete address</description>
</CustomField>Number Formula (Calculated)
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Line_Total__c</fullName>
<label>Line Total</label>
<type>Currency</type>
<precision>18</precision>
<scale>2</scale>
<formula>Quantity__c * Unit_Price__c * (1 - BLANKVALUE(Discount_Percent__c, 0) / 100)</formula>
<formulaTreatBlanksAs>BlankAsZero</formulaTreatBlanksAs>
<description>Calculated line item total with discount</description>
</CustomField>Checkbox Formula
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Is_Overdue__c</fullName>
<label>Is Overdue</label>
<type>Checkbox</type>
<formula>AND(
NOT(ISPICKVAL(Status__c, 'Paid')),
NOT(ISPICKVAL(Status__c, 'Cancelled')),
Due_Date__c < TODAY()
)</formula>
<description>True if invoice is past due date and not paid</description>
</CustomField>---
Roll-Up Summary
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Total_Line_Items__c</fullName>
<label>Total Line Items</label>
<type>Summary</type>
<summarizedField>Order_Line_Item__c.Line_Total__c</summarizedField>
<summaryForeignKey>Order_Line_Item__c.Order__c</summaryForeignKey>
<summaryOperation>sum</summaryOperation>
<description>Sum of all line item totals</description>
</CustomField>Roll-Up with Filter
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Completed_Tasks_Count__c</fullName>
<label>Completed Tasks</label>
<type>Summary</type>
<summaryForeignKey>Project_Task__c.Project__c</summaryForeignKey>
<summaryOperation>count</summaryOperation>
<summaryFilterItems>
<field>Project_Task__c.Status__c</field>
<operation>equals</operation>
<value>Completed</value>
</summaryFilterItems>
<description>Count of completed tasks on this project</description>
</CustomField>---
Summary
| Field Type | Use Case | Key Settings |
|---|---|---|
| Text | Short strings | length (1-255) |
| LongTextArea | Multi-line text | length, visibleLines |
| Number | Quantities, rates | precision, scale |
| Currency | Money | precision, scale |
| Date | Calendar dates | - |
| DateTime | Timestamps | - |
| Checkbox | True/False | defaultValue (required) |
| Picklist | Single choice | valueSet |
| MultiselectPicklist | Multiple choices | valueSet, visibleLines |
| Lookup | Optional relationship | referenceTo, deleteConstraint |
| MasterDetail | Required relationship | referenceTo, relationshipOrder |
| Formula | Calculated | formula, return type |
| Summary | Aggregation | summaryOperation, summarizedField |
<!-- Parent: sf-metadata/SKILL.md -->
Permission Set Auto-Generation
When creating custom objects or fields, default to generating or updating a Permission Set unless the user explicitly opts out.
Core rule
Object CRUD does not make custom fields visible. New metadata work should normally include both:
<objectPermissions>for the object<fieldPermissions>for eligible custom fields
Default workflow
1. Collect field information from the metadata being created or changed 2. Build or update a Permission Set by default for the affected object 3. Include eligible custom fields with the correct read/edit behavior 4. Exclude only fields that Salesforce treats as system-managed or always-available in Permission Set metadata 5. Write the Permission Set to force-app/main/default/permissionsets/[ObjectName]_Access.permissionset-meta.xml 6. Tell the user what was included and excluded
Eligible field guidance
| Field type / category | Include in Permission Set? | Guidance |
|---|---|---|
| Optional custom fields | YES | readable=true, editable=true |
| Formula fields | YES | readable=true, editable=false |
| Roll-up summary fields | YES | readable=true, editable=false |
| Universally required custom fields | Usually NO | Salesforce commonly treats these as always-available / excluded in modern metadata retrievals |
| Master-Detail relationship fields | Usually NO | Commonly treated as parent-controlled / excluded in Permission Set metadata |
| Name / system-managed fields | NO | Do not generate explicit field permissions |
Generated output expectations
A generated Permission Set should:
- include object permissions for the target object
- include field permissions for eligible custom fields
- use read-only field permissions for calculated fields
- clearly document any excluded fields and why they were skipped
Example generated Permission Set
<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<description>Auto-generated: Grants access to Customer_Feedback__c and eligible custom fields</description>
<hasActivationRequired>false</hasActivationRequired>
<label>Customer Feedback Access</label>
<objectPermissions>
<allowCreate>true</allowCreate>
<allowDelete>true</allowDelete>
<allowEdit>true</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>Customer_Feedback__c</object>
<viewAllRecords>true</viewAllRecords>
</objectPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Customer_Feedback__c.Optional_Field__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>false</editable>
<field>Customer_Feedback__c.Score_Band__c</field>
<readable>true</readable>
</fieldPermissions>
</PermissionSet>Opt-out guidance
If the user explicitly says they do not want a Permission Set generated, still call out the exact FLS follow-up they must handle manually.
Implementation note
The generator script under hooks/scripts/generate_permission_set.py should be treated as the default automation path for this workflow.
Related skills
FAQ
What does sf-metadata do?
Apply sf-metadata agent skill workflows from documented SKILL.md guidance.
When should I use sf-metadata?
During build integrations work for automation & workflows.
Is sf-metadata safe to install?
Review the Security Audits panel on this listing before production use.