---
title: "Day one on a greenfield Salesforce org: standing up Field Service without building on sand"
description: "The order I set up Doorvana's Salesforce org — CLI, SFDX project, baseline retrieve, Field Service enablement, the FSL managed package — and what bit me on each."
source: "https://noconaconsulting.com/blog/greenfield-field-service-setup/"
---

# Day one on a greenfield Salesforce org: standing up Field Service without building on sand

_Build log · Doorvana · Garage door manufacturing & service_

The order I set up Doorvana's Salesforce org — CLI, SFDX project, baseline retrieve, Field Service enablement, the FSL managed package — and what bit me on each.

Nocona Consulting · September 22, 2026 · 10 min read

I am building out a Salesforce org for [Doorvana](https://noconaconsulting.com/work), a garage door manufacturer and installer running three lines of business. The org had just moved up to Enterprise Edition, which is what unblocks Field Service, and it was genuinely greenfield — 261 standard objects and not one custom object.

This is what I did on day one, in the order I did it — CLI, project scaffold, baseline retrieve, Field Service, managed package — and the things that cost me time on each. None of them are exotic. All of them are the kind of thing you only find out by hitting them.

## First: get the CLI situation sorted

Before anything else, the toolchain.

An `sfdx-cli` v7.82.0 from 2020 was still installed from the old macOS package installer, root-owned at `/usr/local/lib/sfdx`. Its binary alias collides with `@salesforce/cli`, so the modern `sf` command will not install cleanly over the top of it. The old one has to come off with `sudo` first.

Five minutes once you know. Considerably longer when the error is telling you something about permissions and you are still reading it as a Node problem.

## Scaffolding the SFDX project

`sf project generate` gives you the skeleton in a second. The part worth thinking about is the four files it leaves you to fill in, because each one is a decision you will live with.

### sfdx-project.json

sfdx-project.json

```json
{
  "packageDirectories": [
    {
      "path": "force-app",
      "default": true
    }
  ],
  "name": "doorvana",
  "namespace": "",
  "sfdcLoginUrl": "https://<your-my-domain>.my.salesforce.com",
  "sourceApiVersion": "67.0"
}
```

Two fields here matter more than they look.

`sfdcLoginUrl` should point at the org’s My Domain, not `login.salesforce.com`. Once My Domain is enforced — and it is, on any modern org — the generic login host stops being the right answer and you get an authentication failure that reads like a credentials problem.

`sourceApiVersion` I set provisionally and flagged to verify against the org on first connect. A source API version ahead of what the org actually runs fails deploys in ways that do not name the real cause. It is a one-line check and it saves an afternoon.

### .gitignore — before the first commit, not after

.gitignore

```plaintext
.sf/
.sfdx/
node_modules/
.DS_Store

# Client meeting artifacts — a 5.5 GB discovery recording and screenshots.
# Local only: too large for git, and they contain customer detail.
Meeting Artifacts/
```

That middle entry is the one I would press on. The discovery recording for this project is 5.5 GB. Beyond simply not belonging in git, client recordings and screenshots routinely contain customer names, addresses and pricing. Decide where those live _before_ the first commit, because getting a large file with customer data back out of git history is a bad afternoon and an awkward conversation.

### config/project-scratch-def.json

config/project-scratch-def.json

```json
{
  "orgName": "Doorvana",
  "edition": "Enterprise",
  "features": ["FieldService", "ServiceCloud", "SalesCloud", "Sites"],
  "settings": {
    "lightningExperienceSettings": { "enableS1DesktopEnabled": true }
  }
}
```

Worth writing even if you are not using scratch orgs yet. The `features` array is the shortest honest statement of what this org has to be, and it is checked into the repo where the next person can read it — rather than living in someone’s memory of a licensing call.

### manifest/package.xml

The manifest drives every retrieve, which makes it the source of truth for what “the org” means to this repo. Mine lists the usual types with a wildcard member each: objects, fields, record types, layouts, permission sets, profiles, flows, Apex, tabs, apps, FlexiPages.

That list is also where the first real mistake of the project was hiding — more on that in a moment.

### One fork worth calling out: profiles in source, or not?

The default instinct is to `.forceignore` profiles. They are noisy, they diff badly, and everyone has been burned by them.

I track them anyway on this project, because an org where permissions change and nobody can see the change is exactly the rot I am trying to prevent. That is a real choice with a real cost attached, and the cost shows up two sections down.

## Snapshot the org before you touch it

The first thing into git was not a change. It was the org exactly as it already was.

A baseline retrieve gives you the thing that makes everything afterward reviewable: a diff. Without it, six weeks in, nobody can tell which layout you edited and which one was always like that. With it, every later commit is a specific, readable change.

The snapshot came back as 228 components — 40 Apex classes, 98 layouts, 29 apps, 26 FlexiPages, 24 profiles, 7 permission sets, 2 flows and a tab. All stock. Zero custom objects confirmed what the client had told me: nothing had been built here yet.

Rule of thumb

Baseline first, always. The retrieve that captures the org before your first change is the one that makes every later change a diff instead of an assertion.

## Your “complete” baseline retrieve is not complete

Here is the one that actually bit me.

A `package.xml` with `<members>*</members>` feels exhaustive. It is not. The wildcard applies _within_ each metadata type you listed — a type you did not list is invisible, however many components of it exist in the org.

My first manifest covered the obvious types: objects, fields, record types, layouts, profiles, permission sets, flows, Apex, tabs, apps, FlexiPages. It did not list `GlobalValueSet`. So seven global value sets that already existed in the org simply did not appear, and I spent the first hour believing the org had none.

manifest/package.xml

```xml
<types>
    <members>*</members>
    <name>GlobalValueSet</name>
</types>
<types>
    <members>*</members>
    <name>StandardValueSet</name>
</types>
```

What surfaced once those two entries went in:

| Global value set | Values |
| --- | --- |
| MSA | 29 |
| Property\_Type | 22 |
| Garage\_Door\_Brand | 22 |
| Operator\_Brand | 16 |
| Property\_Status | 8 |
| Garage\_Door\_Material | 5 |
| PMOC | 4 |

This was not housekeeping. `Property_Status` turned out to read as the entire new construction lifecycle — Under Construction, Model Home, Finished Vacant, Owner-Occupied — which maps directly onto the property records I was about to design. And `MSA` sitting at 29 values, rather than the hundreds I had assumed, changed a data model decision I had already half made.

Metadata I could not see was about to make me design the wrong thing.

Do this instead

Do not trust a hand-written manifest. List the metadata types present in the org, diff that list against your manifest, and add what is missing before you call the baseline done.

## Profiles retrieve sparsely, and that is the cost of tracking them

Here is the bill for that choice.

A retrieved profile only carries permissions for components that were part of that same retrieve. Pull a profile alongside two objects and you get a profile file describing permissions for exactly two objects — not a copy of the profile. Deploy that file back and it can silently drop everything it did not mention.

The rule that falls out of it: profiles are retrieved with the full manifest or not at all. Never as part of a narrow, targeted retrieve.

## Enabling Field Service is two steps, not one

This is the part I would most want someone to tell me.

“Turn on Field Service” is actually three separate layers, and only the middle one is the switch in Setup.

_Work Order and Work Order Line Item ship with Service Cloud, so their presence tells you nothing about whether Field Service is on. Enablement creates the scheduling objects. The managed package adds the dispatcher console and the optimizer on top. All three have to be there._

The trap is layer one. `WorkOrder` and `WorkOrderLineItem` are standard Service Cloud objects. They were sitting in the org before Field Service was enabled, and if you check for them you will conclude Field Service is on. It is not. On this org `ServiceAppointment`, `AssignedResource` and `ServiceTerritory` did not exist yet.

Check for `ServiceAppointment`. That is the one that tells you the truth.

Layer three is a separate install with its own version — here, the `FSL` namespace at v262.0.68.1. Worth writing the version down somewhere durable; “we’re on the latest” ages badly.

## Verify enablement with a script, not by squinting at Setup

Rather than clicking through Setup and hoping, I wrote a short check that asks the org directly and prints the whole chain. It takes an org alias so it can run against production or a sandbox.

scripts/check-fsl.sh

```bash
#!/usr/bin/env bash
# Verify Field Service enablement + FSL managed package install.
set -euo pipefail
ORG="${1:-doorvana-prod}"

echo "=== Field Service object chain ($ORG) ==="
sf org list metadata --metadata-type CustomObject --target-org "$ORG" --json \
  | python3 -c "
import json,sys
names={x['fullName'] for x in (json.load(sys.stdin).get('result') or [])}
chain=['WorkOrder','WorkOrderLineItem','ServiceAppointment','AssignedResource',
       'ServiceResource','ServiceTerritory','ServiceTerritoryMember',
       'OperatingHours','TimeSlot','ResourceAbsence','ProductConsumed','Expense']
missing=[o for o in chain if o not in names]
for o in chain:
    print(('  YES  ' if o in names else '  MISS ')+o)
print()
print('Field Service ENABLED' if not missing else f'INCOMPLETE — missing {len(missing)}: '+', '.join(missing))
"

echo
echo "=== FSL managed package ==="
sf package installed list --target-org "$ORG" 2>/dev/null \
  | grep -i 'field service\|FSL' || echo "  FSL managed package NOT installed"
```

Two minutes to write, and it has already earned its keep — every time I wonder whether a deploy landed on the right org, I run it instead of guessing.

## Ignore the managed package — but not with a bare wildcard

Namespaced components belong to the package, not to you. They should not be in your repo, and `.forceignore` is where you say so.

The tempting pattern is `*__*`, because managed components are namespaced with a double underscore. Do not use it. That glob also matches every ordinary custom object you create — `Work_Order_Detail__c` and friends — so it would quietly stop tracking the work you actually own. Name the namespace instead.

.forceignore

```plaintext
# Managed package metadata — namespaced components are owned by the package,
# not by this repo. (Do NOT use a bare *__* glob here: it would also match
# ordinary custom objects like Work_Order_Detail__c.)
**/FSL__*
**/fsl__*
```

## On sandboxes

Enterprise Edition is also what gets you sandboxes — the previous edition had none at all, which is a constraint worth naming out loud when you are scoping an upgrade, because it means there was never anywhere safe to test.

Anything structural gets tried there first. Later the same week I had to decide between two ways of modelling a job-site address, and rather than reason about which was cheaper I ran both against the sandbox and counted: one was a single DML on a single record, the other was three DMLs across two records with a circular reference that made cleanup a three-step operation. That is not an argument I would have won from the documentation.

Budget for it, though. Creating and refreshing a sandbox is not instant, and it is not the thing you want to discover on the morning you need one.

## The order that worked

1.  Remove the old CLI before installing `@salesforce/cli`
2.  Scaffold the SFDX project — and point `sfdcLoginUrl` at the org’s My Domain
3.  Write `.gitignore` before the first commit, with client recordings excluded
4.  Verify `sourceApiVersion` against the org rather than trusting the default
5.  Decide the profiles question deliberately, knowing what it costs
6.  Baseline retrieve the untouched org into git
7.  Reconcile the manifest against the metadata types the org actually has, and retrieve again
8.  Enable Field Service in Setup
9.  Install the Field Service managed package and record its version
10.  Verify the object chain with a script, against the org, not the UI
11.  `.forceignore` the package namespace — specifically, not with a wildcard
12.  Only now start building

Steps 6 and 7 are the ones people skip, and they are the two that decide whether the next three months are reviewable. Getting a manifest wrong is not just a missing file — on this org it nearly cost me a data model decision, made confidently, on incomplete information.

If you are standing up Field Service on a greenfield org and want to compare notes, the [services page](https://noconaconsulting.com/services) says how I work, or just get in touch.

-   Field Service
-   SFDX
-   Greenfield
-   Managed packages
-   Setup
-   Source control

Disclosure: Doorvana's owner is also a co-owner of Nocona Consulting. This is a real engagement between two separate companies, but not an arm's-length one — and not an independent reference. [More on the work page](https://noconaconsulting.com/work).

## FAQ

### What is the difference between enabling Field Service and installing the Field Service managed package?

They are two separate steps and you need both. Enabling Field Service in Salesforce Setup creates the standard objects — Service Appointment, Service Territory, Service Resource, Operating Hours, Assigned Resource and the rest of the chain. Installing the Field Service managed package adds everything in the FSL namespace on top: the dispatcher console, scheduling policies, the optimization engine and the mobile configuration. An org can have Field Service enabled with no managed package installed, which looks like a working install right up until someone asks where the dispatch board is.

### Why do Work Order and Work Order Line Item exist before Field Service is enabled?

Work Order and Work Order Line Item ship with standard Service Cloud, not with Field Service. That makes them a misleading signal: an org can show both objects while Service Appointment, Assigned Resource and Service Territory do not exist at all. Check for Service Appointment rather than Work Order when verifying whether Field Service is actually enabled.

### Why did a wildcard package.xml miss metadata that already existed in the org?

A Salesforce package.xml with a wildcard member only retrieves the metadata types the manifest lists. A type absent from the manifest is invisible to the retrieve, no matter how many components of it exist in the org. On Doorvana's org that hid seven global value sets through the entire baseline retrieve. Enumerate metadata types against the org and reconcile them against the manifest rather than assuming a wildcard retrieve is complete.

### What is the sparse profile retrieve problem in SFDX?

Salesforce profiles retrieve sparsely: a retrieved profile only carries permissions for components that were also part of that same retrieve. Retrieve a profile alongside two objects and you get a profile file describing permissions for two objects. Deploy that file back and it can silently drop every permission it did not mention. Always retrieve profiles with the full manifest, never as part of a narrow retrieve.

### Should Salesforce profiles be tracked in source control?

It is a real fork with a cost either way. Tracking profiles in an SFDX repo means permission changes are reviewable and revertible, which is the main defence against an org quietly rotting. The cost is the sparse retrieve: a profile only carries permissions for components included in the same retrieve, so profiles must always be retrieved with the full manifest or a redeploy can drop permissions it never saw. Excluding profiles avoids that trap but leaves permission changes invisible in the repo. On the Doorvana project profiles are tracked, deliberately.

### What should sfdcLoginUrl be set to in sfdx-project.json?

Point sfdcLoginUrl at the org's own My Domain URL rather than login.salesforce.com. Once My Domain is enforced, which it is on any modern org, the generic login host stops working and the resulting authentication failure reads like a credentials problem rather than a configuration one.

### Should the Field Service managed package be excluded from an SFDX repo?

Yes. Namespaced components are owned by the package, not by the repo, so they belong in .forceignore. Exclude the specific namespace — FSL\_\_\* — and never a bare \*\_\_\* glob, because that pattern also matches ordinary custom objects and would quietly stop tracking the work you actually own.
