---
title: "Two doors, one trip fee, and the fields that were on the wrong object"
description: "A warranty network's diagnosis checklist was planned as seven fields on WorkOrderLineItem. Two of them belong on the Work Order instead, because the trip fee is charged once per visit however many doors get diagnosed — and the question that settles it is cardinality, not the requirement document."
source: "https://noconaconsulting.com/blog/work-order-vs-line-item-field-grain/"
---

# Two doors, one trip fee, and the fields that were on the wrong object

_Architecture · Doorvana · Garage door manufacturing & service_

A warranty network's diagnosis checklist was planned as seven fields on WorkOrderLineItem. Two of them belong on the Work Order instead, because the trip fee is charged once per visit however many doors get diagnosed — and the question that settles it is cardinality, not the requirement document.

Nocona Consulting · October 13, 2026 · 8 min read

A technician arrives at a house with two failed garage doors. One needs a spring; the other needs the whole operator replaced. He is diagnosing both for a home-warranty network, which requires seven specific items documented on every diagnosis before it will pay anything.

The trip fee was charged once. He drove to one address.

That sentence is the whole design problem, and I nearly missed it. The implementation plan — written by me, weeks earlier — listed the network’s seven requirements and put all seven on `WorkOrderLineItem`. Building it that way would have compiled, deployed, and stored the same trip-fee answer twice on every two-door visit, with nothing defining what happens when the two copies disagree.

Four of those seven requirements turned out to be answerable from standard fields nobody had looked at, which is [its own story and has its own post](https://noconaconsulting.com/blog/builder-owner-warranty-lookup/). This one is about the three that genuinely needed building, and the question that decided where they go.

## The question that picks the object: how many can there be?

The requirement document is no help here. A warranty network does not care which object anything sits on; it cares that the answers arrive. So the placement decision has to come from the shape of the work, and in Field Service the shape is already fixed:

-   A `WorkOrder` is **one visit to one place**.
-   A `WorkOrderLineItem` is **one piece of work**, and on this model, one opening — one `Asset`, through the standard `WorkOrderLineItem.AssetId`.

Then ask each fact how many answers it can have in a single visit.

_Cause of failure_ — two doors, two causes. Per line. _Repair or replace_ — the spring gets repaired and the operator gets replaced, on the same trip, in the same hour. Per line. _Was a service fee collected_ — he drove to one address and charged for one trip. Per visit. _Does that fee credit against the repair if the customer goes ahead_ — same fee, same answer. Per visit.

_One trip, one Work Order, one trip-fee answer. Two openings, two line items, two diagnoses. A field placed at the wrong grain either stores the same answer twice or forces two answers into one._

Two checkboxes went on `WorkOrder`, and the field description carries the reasoning so the next person does not have to re-derive it:

force-app/main/default/objects/WorkOrder/fields/Service\_Fee\_Collected\_\_c.field-meta.xml

```xml
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
    <fullName>Service_Fee_Collected__c</fullName>
    <label>Service Fee Collected</label>
    <description>Whether a service (trip) fee was collected. On the Work Order, not the
        line item, because the fee is charged once per visit however many doors are
        diagnosed. This records the answer the warranty network asks for; the fee itself
        must still be a priced line so it reaches Order Products.</description>
    <inlineHelpText>Check if the customer paid a service fee for this visit.</inlineHelpText>
    <type>Checkbox</type>
    <defaultValue>false</defaultValue>
</CustomField>
```

That last clause matters more than it reads. A checkbox saying a fee was collected is documentation, not money. The fee still has to exist as a priced `WorkOrderLineItem` for the billing bridge to carry it onto the Order, and the same is true of the credit when the fee applies toward the repair. [The bridge only bills what carries a price](https://noconaconsulting.com/blog/product-consumed-isconsumed-billing-filter/), which is exactly the property you want here: a checkbox can never silently become an invoice line, and a fee that was never charged shows up as a missing priced line rather than as two fields disagreeing about the truth.

Rule of thumb

Before adding a field, ask how many times its answer can be true within one instance of the parent. If the answer is “once, no matter how much work happened”, the field is a property of the parent. A per-visit fact stored per line is duplicated data with no rule for disagreement, and the disagreement always arrives eventually — a technician who ticks the box on one line and not the other, and a report that now depends on which line it reads.

The two line-item fields got the same treatment in the other direction. `Cause_of_Failure__c` is a 2,000-character long text area rather than a picklist, and its description records that as a reversible choice: if failure-cause reporting ever becomes a requirement, add a restricted picklist beside it rather than converting the field — converting a text field to a picklist on a global value set [is not a thing the platform will do in place](https://noconaconsulting.com/blog/job-site-address-circular-delete/). `Recommendation__c` is restricted with exactly two values, Repair and Replace, because that is the entire universe the network recognises and free text there would be a reporting problem from the first record.

## The help text that described a feature nobody had built

Earlier the same day, `WorkOrder.Bill_To_Account__c` had been created with this inline help text:

> The account that pays. Defaults to the Work Order’s Account; change it when someone else, such as a landlord, is paying.

Nothing defaulted it. The field was a plain lookup and the sentence was aspirational — a description of behaviour that existed only in the head of whoever wrote the help text. The platform does not check that claim, and the person most likely to believe it is a dispatcher who leaves the field empty because the tooltip promised it fills itself. The field is genuinely needed, incidentally: the caller is often not the payer, because a renter reports the broken door and the landlord pays for it unless the tenant caused the damage.

The fix is a before-save record-triggered flow, and the interesting part is that it contains no decision element. The condition lives in the entry criteria:

force-app/main/default/flows/Work\_Order\_Default\_Bill\_To.flow-meta.xml

```xml
<start>
    <connector>
        <targetReference>Bill_To_Work_Order_Account</targetReference>
    </connector>
    <filterLogic>and</filterLogic>
    <filters>
        <field>Bill_To_Account__c</field>
        <operator>IsNull</operator>
        <value><booleanValue>true</booleanValue></value>
    </filters>
    <filters>
        <field>AccountId</field>
        <operator>IsNull</operator>
        <value><booleanValue>false</booleanValue></value>
    </filters>
    <object>WorkOrder</object>
    <recordTriggerType>CreateAndUpdate</recordTriggerType>
    <triggerType>RecordBeforeSave</triggerType>
</start>
```

Because the flow cannot fire on a record that already has a Bill To, it is safe to run on update as well as create. That matters: Work Orders generated from a Maintenance Plan, or migrated in, can arrive with no Account and be given one later, and a create-only default would miss every one of them. A landlord typed in by hand survives every subsequent save, because the flow never runs on that record again.

`Bill_To_Contact__c` is deliberately _not_ defaulted. Blank there means invoice the Work Order’s Contact, which is a meaningful answer rather than a gap — and a default that filled it would make “the usual person” and “this specific person” indistinguishable.

Help text is not an implementation

Inline help text renders a tooltip. It does not default, calculate, or maintain anything. Grep a repo’s field metadata for `defaults`, `automatically` and `calculated` in `description` and `inlineHelpText`, then confirm each hit against a real flow, formula or class. Every one that fails is a promise the org is making to its users on your behalf.

## One more field that was pointing at too much

`Pickup_Location__c` on `WorkOrder` records where the technician collects product before the job. This company holds no inventory — technicians collect from vendor sites on the way to the first call — so where to go has to be on the technician’s Work Order rather than implied by a warehouse the company does not have.

A lookup to `Location` offers every Location in the org, and in this data model that includes every customer’s house, because a customer property [is a Location record too](https://noconaconsulting.com/blog/household-accounts-without-person-accounts/). So the lookup carries a required filter:

force-app/main/default/objects/WorkOrder/fields/Pickup\_Location\_\_c.field-meta.xml

```xml
<lookupFilter>
    <active>true</active>
    <errorMessage>Pickup Location must be a Warehouse location.</errorMessage>
    <filterItems>
        <field>Location.LocationType</field>
        <operation>equals</operation>
        <value>Warehouse</value>
    </filterItems>
    <isOptional>false</isOptional>
</lookupFilter>
```

`isOptional` false is the part worth noticing. An optional lookup filter is a suggestion the user can switch off in the lookup dialog; a required one is enforced at save with that error message. For a field whose wrong values are all customer addresses, the suggestion is not good enough.

Sixteen components passed a `sf project deploy start --dry-run`, then deployed to the sandbox, and the three things worth clicking all held: a blank Bill To filled from the Account on save, a hand-entered Bill To survived save and reload, and the Pickup Location lookup hid a customer property while offering a warehouse.

## Practical summary

-   Choose between a parent and a child object by cardinality per parent, not by which object the requirement document happens to mention. One trip, one trip fee; two openings, two diagnoses.
-   In Field Service the `Asset` link lives on `WorkOrderLineItem`, which is what lets one visit cover several pieces of equipment. Design for two before you meet the second one.
-   Keep documentation fields and money strictly apart. A fee is a priced line item; a checkbox recording that it was collected is paperwork, and neither should be able to impersonate the other.
-   Put a default’s condition in a before-save flow’s entry criteria rather than a decision element. The flow then cannot overwrite a human’s value, which is what makes it safe on update as well as create.
-   Leave a field undefaulted when blank already means something.
-   Filter a lookup whenever the field means something narrower than the object it points at, make the filter required rather than optional, and write the error message as a sentence the user can act on.
-   Help text is a tooltip, not an implementation. Every behavioural claim in a description or a tooltip should be traceable to something that actually runs.

The pattern I keep hitting on this project is that the requirement document and the platform describe the world at different resolutions. The document says “document whether a service fee was collected”. The platform asks: collected for what — this door, or this trip? Answering that is most of the modelling work, and it is not in the document.

-   Field Service
-   WorkOrderLineItem
-   WorkOrder
-   Data model
-   Before-save flow
-   Lookup filters

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

### Should a field go on WorkOrder or WorkOrderLineItem in Salesforce?

Decide by how many times the fact can be true per visit. A Work Order is one visit to one place; a Work Order Line Item is one piece of work, usually against one Asset. A fact answered once per trip — was a trip fee collected, does it credit against the repair, who is paying — belongs on the Work Order. A fact that can differ between two items worked in the same visit — what failed, whether to repair or replace, which serial number — belongs on the line item. Putting a per-visit field on the line item stores the same answer twice with no rule for what happens when the two copies disagree.

### Can one Salesforce Work Order cover work on several Assets?

Yes, and that is the normal case in Field Service. The Asset lookup lives on WorkOrderLineItem, not on WorkOrder, precisely so one visit can carry several pieces of work against several Assets. A Work Order also has an AssetId of its own, which is useful when a visit really is about one piece of equipment, but relying on it as the only Asset link is what quietly caps an org at one item per visit.

### Does a documentation checkbox on a Work Order affect billing in Salesforce?

No, and it should not. A checkbox recording that a trip fee was collected answers a paperwork question. The money only moves if the fee exists as a priced line item that a billing process turns into an Order Product. Keeping those separate is a feature: a checkbox can never silently become an invoice line, and a missing fee shows up as a missing priced line rather than as a discrepancy between two fields that both claim to be authoritative.

### Can a before-save flow default a lookup field only when it is blank?

Yes, and the condition belongs in the flow's entry criteria rather than in a decision element inside it. Set the record-triggered flow's start filters to the field being null plus whatever else must be true, and the flow simply never runs on a record that already has a value. That is what makes it safe to run on update as well as create: a value a human typed cannot be overwritten, because the flow does not fire on that record again.

### Does inline help text on a Salesforce field do anything?

It renders a tooltip. That is all. Help text describing behaviour — that a field defaults from somewhere, recalculates, or is maintained by automation — is a claim about the org that nothing in the field definition enforces, and the person most likely to believe it is the user who leaves the field blank because the tooltip said it fills itself. Grep a repo's field metadata for defaults, automatically and calculated in description and inlineHelpText, and confirm each hit against a real flow, formula or class.

### Should a lookup filter be used to restrict a Salesforce Location lookup?

Yes, when the lookup means something narrower than the object. A Location record can be a customer property, a warehouse or a van, so an unfiltered lookup to Location offers all of them — including every customer's house. A required lookup filter on LocationType with a custom error message turns a data-entry mistake into a save-time rejection with a sentence the user can act on, instead of a plausible-looking record that breaks something downstream.
