Series Note: This article is Part 2 of our hands-on engineering series: Building the Salesforce Appointment Scheduler. Follow along as we design and implement a multi-location scheduling platform on Salesforce using enterprise patterns and developer-first tooling.
The “Admin-by-Default” Anti-Pattern in Salesforce DX
In a well-architected Salesforce DX project, we design for least-privilege access from Day 1. Every custom capability should ship with dedicated, granular permission sets rather than relying on bloated System Administrator profile overrides.
However, anyone who has built permission sets knows the pain:
- The Web UI Setup Crawl: You navigate to Setup → Permission Sets → Object Settings. For seven objects, you must click into each object page, click Edit, tick Read, Create, Edit, Delete, View All, scroll down through dozens of fields to check Read and Edit for each, click Save, wait for the redirect, and repeat seven times.
- The “Everything or Nothing” Scratch Org Trap: Developers often skip building permission sets until the end of a project because it is too tedious. Instead, they develop as System Administrator-only to discover during UAT or production deployment that users cannot see fields, buttons, or records due to missing Field-Level Security (FLS).
- Manual XML Merge Conflicts: Crafting a
.permissionset-meta.xmlmanually means writing repetitive<objectPermissions>,<fieldPermissions>, and<recordTypeVisibilities>blocks, where a single misspelled field or duplicate tag causes cryptic deployment errors.
In Part 2 of building the Salesforce Appointment Scheduler, we address security immediately using Hob: The Salesforce House-Elf (hob create permset).
The Persona: Appointment_Scheduler_Operator
Our application is operated by headquarters coordination staff who handle customer inquiries, match them with nearby branch locations, and book service appointments with qualified technicians.
To perform their role, an Operator requires:
- Full CRUD & View All on our four custom objects:
Appointment__cService_Type__cStaff_Service_Type__cStaff_Working_Hours__c
- Read / Create / Edit / View All on standard CRM entities:
Account(Branch Locations)Contact(Customers and Location Staff)Task(Follow-up activities)
- Field-Level Security (FLS) on custom fields.
- Record Type Visibility for
Account.Location,Contact.Customer, andContact.Location_Staff.
Scaffolding the Permission Set with Hob
Hob provides a dedicated command to scaffold permission sets with multi-object permissions pre-wired in seconds:
hob create permset Appointment_Scheduler_Operator \
-l "Appointment Scheduler Operator" \
-d "Grants access to Appointment Scheduler objects, fields, and operations" \
--objects Appointment__c Service_Type__c Staff_Service_Type__c Staff_Working_Hours__c Account Contact Task
The Terminal Output
✨ Hob - The Salesforce House-Elf ✨
"Loyal, quiet, and tireless assistance for your Salesforce Development."
✔ Created Permission Set 'Appointment_Scheduler_Operator'
🪄 Hob's wand granted the permission set boilerplate:
Name: Appointment_Scheduler_Operator
Label: Appointment Scheduler Operator
Objects: Appointment__c, Service_Type__c, Staff_Service_Type__c, Staff_Working_Hours__c, Account, Contact, Task
File: force-app/main/default/permissionsets/Appointment_Scheduler_Operator.permissionset-meta.xml
👉 Assign to your active org with: sf org assign permset -n Appointment_Scheduler_Operator
With one command, Hob created Appointment_Scheduler_Operator.permissionset-meta.xml and pre-configured complete <objectPermissions> tags for all seven objects:
<objectPermissions>
<allowCreate>true</allowCreate>
<allowDelete>true</allowDelete>
<allowEdit>true</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>Appointment__c</object>
<viewAllRecords>true</viewAllRecords>
</objectPermissions>
Deep Dive: The Critical Rules of Salesforce FLS & Required Fields
While object-level permissions provide the entry gate, non-admin users in Salesforce cannot read or write data without Field-Level Security (FLS) and Record Type Visibilities.
Here we encounter an important Salesforce metadata nuance that catches many developers off-guard:
1. The “Universally Required” Field Trap
In our Part 1 schema, fields like Appointment__c.Customer__c, Appointment__c.Start_Time__c, and Service_Type__c.Duration_Minutes__c were scaffolded with <required>true</required>.
In Salesforce metadata rules:
Universally required fields cannot have
<fieldPermissions>defined in a permission set.
If you include a required field in a permission set’s <fieldPermissions> block, the Metadata API deployment will immediately reject it with:
Error: Cannot specify fieldPermissions for a required field
Because a required field is mandatory across the entire database, Salesforce automatically grants Read and Edit access to any user who has access to the parent object.
2. Granting FLS for Non-Required Fields
For optional and checkbox fields, explicit <fieldPermissions> are mandatory. We include permissions for all optional custom fields:
Account.Active__cAppointment__c.Notes__cService_Type__c.Active__cService_Type__c.Description__cStaff_Service_Type__c.Active__cStaff_Working_Hours__c.Active__c
<fieldPermissions>
<editable>true</editable>
<field>Appointment__c.Notes__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Account.Active__c</field>
<readable>true</readable>
</fieldPermissions>
3. Record Type Visibilities
To enable operators to create and view branch accounts and distinct customer vs. staff contacts, we expose our record types:
<recordTypeVisibilities>
<recordType>Account.Location</recordType>
<visible>true</visible>
</recordTypeVisibilities>
<recordTypeVisibilities>
<recordType>Contact.Customer</recordType>
<visible>true</visible>
</recordTypeVisibilities>
<recordTypeVisibilities>
<recordType>Contact.Location_Staff</recordType>
<visible>true</visible>
</recordTypeVisibilities>
Developer Experience (DX) & Tool Feedback
What Shined
- Multi-Object Syntax: Passing multiple objects (
--objects Obj1 Obj2 Obj3) in one pass saved creating and configuring seven individual permission blocks. - Clean, Minimal XML: The generated file is deterministic, formatted with standard indentation, and free of extraneous profile junk.
- Instant CLI Feedback: Hob reminded us immediately of the assignment command (
sf org assign permset -n Appointment_Scheduler_Operator), shortening the cognitive loop between scaffolding and deployment.
Opportunities for Hob Enhancement
- Automatic FLS Discovery (
--include-fieldsor--auto-fls):- Hob could inspect the local
force-appdirectory for the specified objects, detect all custom fields, automatically filter out universally required fields (<required>true</required>), and generate appropriate<fieldPermissions>.
- Hob could inspect the local
- Record Type Visibility Flag (
--record-types):- Allowing flags like
--record-types Account.Location Contact.Customerto automatically add<recordTypeVisibilities>.
- Allowing flags like
The Appointment Scheduler Series
- Series Index: Building the Salesforce Appointment Scheduler: Architecture & Series Index
- ← Part 1: Eliminating Salesforce XML Fatigue — CLI Schema Scaffolding with Hob
- Part 2: Instant Least-Privilege Access — Generating Permission Sets with Hob (Current Article)
- Part 3: Standardizing Test Hygiene — In-Memory vs DML Factories with Hob →
- Part 4: High-Velocity TDD — Companion Test Scaffolding with Hob
- Part 5: Architecture by Default — Separation of Concerns with Hob Triggers
- Part 6: Building Modern Lightning UIs Fast — Scaffolded LWCs with Hob
- Part 7: One Command to Rule Them All — The Complete Dev Org Setup with Hob