QEAGENTS ← Back to site

Salesforce Conflict Resolution

Merging Apex and declarative work into a shared org

Salesforce Conflict Resolution AI - Agent

Salesforce orgs are changed by two populations who rarely share a workflow. When their work meets in a release, the result is a metadata conflict that no merge tool can settle on its own — because the decision that matters is usually not in the file being merged.

Pro-code

Apex developers

Work in feature branches. Write Apex classes, triggers and Lightning components. Changes arrive as pull requests and get reviewed as code, by people who read code.

No-code

Declarative builders

Work in the Salesforce UI. Change fields, validation rules, flows and layouts directly in a sandbox. The platform generates the metadata XML afterwards — often without ever passing through a branch or a review.

Both are legitimate ways to build on the platform. The problem is the seam between them: a declarative change is a real change to production behaviour, but it arrives with none of the review apparatus that a code change carries. When the two meet at a deployment boundary, someone is asked to reconcile them — usually under time pressure, usually fluent in only one of the two stacks.

The conflict

A pro-code branch adds Next_Stage__c to the Case object alongside the Apex that populates it. In parallel, an administrator adjusts the same field directly in the UAT sandbox. Both changes are saved. Both are correct in isolation.

3 conflicts in this file. The same field was changed in a feature branch and in the target environment. Each difference must be resolved before the deployment can proceed.

Next_Stage__c.field-meta.xml
force-app/main/default/objects/Case/fields/Next_Stage__c.field-meta.xml
3 / 3 resolved
Pro-code branch
feature/case-next-stage
1<?xml version="1.0" encoding="UTF-8"?>
2<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
3 <fullName>Next_Stage__c</fullName>
4 <complianceGroup>PII;GDPR</complianceGroup>
5 <externalId>false</externalId>
6 <label>Next stage</label>
7 <length>200</length>
8 <required>false</required>
9 <trackFeedHistory>false</trackFeedHistory>
10 <trackHistory>false</trackHistory>
11 <trackTrending>false</trackTrending>
12 <type>Text</type>
13 <unique>false</unique>
14</CustomField>
Target environment
uat · declarative changes
1<?xml version="1.0" encoding="UTF-8"?>
2<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
3 <fullName>Next_Stage__c</fullName>
METADATA HERE WAS DELETED
4 <externalId>false</externalId>
5 <label>Next stage</label>
6 <length>100</length>
7 <required>true</required>
8 <trackFeedHistory>false</trackFeedHistory>
9 <trackHistory>false</trackHistory>
10 <trackTrending>false</trackTrending>
11 <type>Text</type>
12 <unique>false</unique>
13</CustomField>

What a merge tool can see, and what it cannot

A three-way merge presents these as three equivalent choices: keep the left value or the right one, three times, then continue. That framing is the problem. It gives a data protection classification exactly the same visual weight as an integer, and it shows the file in isolation from everything that depends on it.

The dependency that decides conflict two is not in the file at all:

public class CaseStageHandler {
    private static final Integer MAX_STAGE_LEN = 200;

    public static void applyNextStage(List<Case> cases) {
        for (Case c : cases) {
            String stage = buildStageLabel(c);
            c.Next_Stage__c = stage.abbreviate(MAX_STAGE_LEN);
        }
    }
}

force-app/main/default/classes/CaseStageHandler.cls

And the dependency that decides conflict three is in a test:

// 3 tests insert Case without Next_Stage__c
@isTest
static void escalationWithoutNextStage() {
    Case c = new Case(Subject = 'Escalation');
    insert c;
}

force-app/main/default/classes/CaseStageHandlerTest.cls

The conflict is not inside the XML. It is between the XML and the Apex that depends on it — and the merge view cannot show you that.

The administrator who set length to 100 had no way to see CaseStageHandler. The developer who wrote abbreviate(200) had no way to see a sandbox change made three weeks later. Neither made a mistake. The seam between them did.

Resolution

Each conflict is resolved against the change that requested it and against the code on both sides of the platform, not against whichever version happens to be newer.

01 <complianceGroup> Restored
branch · PII;GDPR environment · deleted

Keep PII;GDPR.

The classification was not removed by a decision. It was dropped as a side effect of a declarative save in an org where compliance categorisation was never enabled in the UI. Nothing in the originating change asked for it, and removing a data protection tag from a customer-facing field is not something that should happen silently as the third checkbox in a merge screen.

02 <length> Branch value
branch · 200 environment · 100

Keep 200.

CaseStageHandler.applyNextStage() abbreviates to a constant of 200 before assigning. Accepting 100 does not fail the deployment — it fails later, at runtime, as a truncation on records whose stage labels exceed the shorter limit. This is the resolution a metadata-only view is least equipped to make, because the deciding evidence is in an Apex class the merge never opens.

03 <required> Escalated
branch · false environment · true

Not resolved automatically. Raised for a human decision.

Making the field required breaks three Apex tests that insert Case without it — so the mechanical answer is to keep false. But the administrator may have set it deliberately, to enforce a process that matters more than the tests do. That is a business question, not a merge question. The agent reports both consequences, names the three failing tests, and leaves the decision with the people who own the process.

What this caught

ConflictIf resolved by newest winsOutcome
complianceGroup PII;GDPR tag silently dropped in UAT and every environment promoted after it Compliance regression, invisible until an audit
length Field shortened to 100 while Apex still writes up to 200 Runtime truncation on live records
required Field made mandatory without reviewing what inserts it Three test failures, and an unexamined process change

Two of the three would have deployed cleanly. Neither would have appeared in a pull request, because neither began as code. The review that catches them has to read the metadata and the Apex together, and has to know which of the three differences is a compliance control rather than a configuration value.

Salesforce Conflict Resolution AI - Agent · QEAGENTS