Study · Inside the model
An MRI of a model doing Salesforce work
Forcebench grades what models write. This study looks inside one while it works: Qwen3.8 27B, an open-weights model with 64 layers, read layer by layer as it writes Apex, weighs up languages and reads Forcebench’s own tasks. The tools are the ones explained on Interpretability 101, and the code and every number are on GitHub.
Does it think in Java while writing Apex?
Not as such. Until about layer 50 of 64, its guess for the next token follows the one it makes when no language is named, and that default is Java’s in 92% of pairs. Apex is applied after that.
Does it reach for code when clicks would do?
Rarely. With nobody named, it proposes code for 1 of the 24 requirements that clicks can do, and for 11 of the 12 that need it. When a developer asks, it reaches for code more often, and it still suggests tools Salesforce has retired.
Does it know it’ll fail?
Its activations, before it starts, tell the tasks it will fail from the ones it will pass better than the suite alone does. Asked, it rates its chances 7.6 of 9 against a 29% pass rate.
How it was looked at
The model is Qwen3.8 27B in MLX 8-bit, the same weights Forcebench grades as qwen3.8-27b-mlx-8bit. It ran locally with its internals exposed: after each of its 64 layers, the activations (the residual stream) can be read, swapped for another run’s or nudged in a direction. No new answers were graded: study 3 uses the grades Forcebench already has for this model. Reasoning was off unless a section says otherwise.
- The logit lens reads what the model would say next if it stopped at a given layer.
- Linear probes test whether something can be read off the activations at a layer, on examples held out of their training.
- Activation patching swaps one run’s activations into another to see where a decision is made.
- Steering adds a direction to the activations to check that it does what a probe says it means.
01 · Apex and Java
Does it think in Java while writing Apex?
Apex looks like Java, and there is far more Java to learn from, so a model could plausibly draft in Java and translate. To test that, 64 short coding requests were each written three ways: asking for Apex, asking for Java, and naming no language. Each stops at a point where the two languages part ways, such as System. before debug( or out.println(, across 10 kinds of idiom. At that point the logit lens reads, at every layer, which language’s next token is ahead.
- Asked for Apex
- Asked for Java
- No language named
Show the numbers
| Layer | Asked for Apex | Asked for Java | No language named | 95% interval, asked for Apex |
|---|---|---|---|---|
| 0 | +0.8 | +0.8 | +0.8 | 0.6 to 1.5 |
| 1 | +0.8 | +0.8 | +0.9 | 0.5 to 1.2 |
| 2 | +0.8 | +0.6 | +0.7 | 0.6 to 1.1 |
| 3 | +0.8 | +0.5 | +0.6 | 0.4 to 1.1 |
| 4 | +0.8 | +0.8 | +0.7 | 0.5 to 1.0 |
| 5 | +0.8 | +0.7 | +0.7 | 0.5 to 1.1 |
| 6 | +0.7 | +0.6 | +0.5 | 0.4 to 1.0 |
| 7 | +0.6 | +0.5 | +0.6 | 0.4 to 0.8 |
| 8 | +0.7 | +0.6 | +0.7 | −0.0 to 0.8 |
| 9 | +0.4 | +0.6 | +0.7 | −0.4 to 0.8 |
| 10 | +0.3 | +0.6 | +0.7 | −0.6 to 0.9 |
| 11 | +0.3 | +0.5 | +0.6 | −0.5 to 0.7 |
| 12 | +0.3 | +0.5 | +0.6 | −0.3 to 0.4 |
| 13 | +0.1 | +0.5 | +0.5 | −0.4 to 0.5 |
| 14 | +0.1 | +0.4 | +0.2 | −0.3 to 0.6 |
| 15 | +0.1 | +0.2 | +0.0 | −0.3 to 0.6 |
| 16 | +0.1 | +0.4 | +0.1 | −0.4 to 0.4 |
| 17 | −0.1 | +0.1 | +0.1 | −0.5 to 0.2 |
| 18 | +0.1 | +0.1 | +0.1 | −0.2 to 0.2 |
| 19 | −0.2 | −0.2 | −0.1 | −0.7 to 0.2 |
| 20 | −0.0 | −0.3 | −0.3 | −0.7 to 0.2 |
| 21 | −0.2 | −0.1 | −0.2 | −0.4 to 0.5 |
| 22 | +0.1 | −0.2 | −0.2 | −0.2 to 0.8 |
| 23 | +0.2 | −0.3 | −0.2 | −0.7 to 0.7 |
| 24 | −0.3 | −0.6 | −0.6 | −0.6 to 0.2 |
| 25 | −0.2 | −0.6 | −0.7 | −0.6 to 0.0 |
| 26 | −0.2 | −0.7 | −0.8 | −0.9 to 0.3 |
| 27 | −0.2 | −0.6 | −0.8 | −0.7 to 0.6 |
| 28 | −0.1 | −0.7 | −0.7 | −0.6 to 0.8 |
| 29 | −0.2 | −0.7 | −0.9 | −0.4 to 0.6 |
| 30 | −0.1 | −0.6 | −0.8 | −0.5 to 0.5 |
| 31 | −0.1 | −0.6 | −0.7 | −0.5 to 0.5 |
| 32 | +0.1 | −0.8 | −0.8 | −0.3 to 0.4 |
| 33 | +0.0 | −0.7 | −0.6 | −0.8 to 0.7 |
| 34 | +0.0 | −0.6 | −0.7 | −0.5 to 0.7 |
| 35 | +0.0 | −0.7 | −0.8 | −0.6 to 0.3 |
| 36 | +0.2 | −0.9 | −1.0 | −1.3 to 0.9 |
| 37 | −0.1 | −1.3 | −1.1 | −0.9 to 1.0 |
| 38 | −0.4 | −1.0 | −1.1 | −1.3 to 0.7 |
| 39 | −0.2 | −0.9 | −1.0 | −1.4 to 0.8 |
| 40 | +0.1 | −1.2 | −0.9 | −0.9 to 0.6 |
| 41 | +0.1 | −0.8 | −0.9 | −1.0 to 0.8 |
| 42 | +0.1 | −1.0 | −0.8 | −0.8 to 0.9 |
| 43 | +0.0 | −1.0 | −0.9 | −0.8 to 0.4 |
| 44 | −0.4 | −0.9 | −1.4 | −1.0 to 0.4 |
| 45 | −0.8 | −1.5 | −1.8 | −1.4 to 0.3 |
| 46 | −1.1 | −1.8 | −2.0 | −1.7 to 0.2 |
| 47 | −0.6 | −1.6 | −1.9 | −1.2 to 0.3 |
| 48 | −0.3 | −1.5 | −1.6 | −0.9 to 0.1 |
| 49 | −0.4 | −1.7 | −1.5 | −1.0 to −0.1 |
| 50 | −1.4 | −2.7 | −3.2 | −2.3 to −0.6 |
| 51 | −2.1 | −3.1 | −3.2 | −3.1 to −1.4 |
| 52 | −1.9 | −5.1 | −4.8 | −3.1 to −1.7 |
| 53 | −2.5 | −5.6 | −5.8 | −3.8 to −2.2 |
| 54 | −2.0 | −6.7 | −6.5 | −2.9 to −1.4 |
| 55 | −2.0 | −7.2 | −7.3 | −3.5 to −1.3 |
| 56 | −1.2 | −9.4 | −8.8 | −1.9 to 0.5 |
| 57 | +1.0 | −11.9 | −10.9 | −0.2 to 2.3 |
| 58 | +4.5 | −12.2 | −11.3 | 3.5 to 5.9 |
| 59 | +5.8 | −12.2 | −11.0 | 4.9 to 6.7 |
| 60 | +5.3 | −12.4 | −11.9 | 4.6 to 6.3 |
| 61 | +6.7 | −11.9 | −11.0 | 5.9 to 8.4 |
| 62 | +9.2 | −11.6 | −10.6 | 7.9 to 9.9 |
| 63 | +8.9 | −9.6 | −7.2 | 6.7 to 10.1 |
Asked for Apex, the model ends with the Apex token ahead in 60 of 64 pairs. On the way there, from layer 49 to 55, the Java token is ahead, most at layer 53 (−2.5), where it leads in 75% of pairs. Apex takes over at layer 57 and finishes far ahead.
That dip is not the model translating from Java as such. Asked for no language at all, it reads like the Java request to the end, and it picks the Java form in 92% of pairs: Java is its default for this kind of code. Up to layer 50, the Apex request follows that default closely, pair by pair: a correlation of at least 0.81 from layer 24, and never more than 0.8 log-odds apart in the median. From layer 49 the default swings hard towards the Java token, and the Apex correction follows a few layers behind; in between, Java is ahead. Where the default is Apex’s own form, string equality with == (5 pairs), the middle layers favour == even when asked for Java. So the middle of the network drafts the most likely code with little regard for the language asked for, and the language is applied late.
The model does know the language early. A linear probe on the same position tells the Apex request from the Java one with at least 96% accuracy from layer 1 on, on pairs it wasn’t trained on. It just doesn’t use that for the next token until about layer 50.
- Probe reads the language
- Apex token ahead
Show the numbers
| Layer | Probe accuracy | Apex token ahead | 95% interval |
|---|---|---|---|
| 0 | 90% | 69% | 56–80% |
| 1 | 96% | 69% | 56–80% |
| 2 | 98% | 66% | 53–77% |
| 3 | 99% | 69% | 56–80% |
| 4 | 100% | 69% | 56–80% |
| 5 | 98% | 69% | 56–80% |
| 6 | 98% | 69% | 56–80% |
| 7 | 100% | 64% | 52–75% |
| 8 | 99% | 61% | 48–72% |
| 9 | 98% | 55% | 42–67% |
| 10 | 98% | 55% | 42–67% |
| 11 | 98% | 55% | 42–67% |
| 12 | 98% | 55% | 42–67% |
| 13 | 99% | 53% | 41–66% |
| 14 | 98% | 53% | 42–66% |
| 15 | 98% | 53% | 42–66% |
| 16 | 100% | 56% | 44–69% |
| 17 | 100% | 44% | 31–56% |
| 18 | 100% | 58% | 45–70% |
| 19 | 100% | 48% | 36–61% |
| 20 | 100% | 48% | 36–61% |
| 21 | 100% | 44% | 31–56% |
| 22 | 100% | 56% | 45–69% |
| 23 | 100% | 53% | 41–66% |
| 24 | 100% | 45% | 33–58% |
| 25 | 100% | 41% | 30–53% |
| 26 | 100% | 47% | 34–59% |
| 27 | 100% | 47% | 34–59% |
| 28 | 100% | 47% | 34–59% |
| 29 | 100% | 45% | 33–58% |
| 30 | 100% | 48% | 36–61% |
| 31 | 100% | 48% | 36–61% |
| 32 | 100% | 52% | 41–64% |
| 33 | 100% | 50% | 39–63% |
| 34 | 100% | 50% | 38–63% |
| 35 | 100% | 52% | 39–64% |
| 36 | 98% | 53% | 41–66% |
| 37 | 99% | 48% | 36–61% |
| 38 | 98% | 42% | 30–55% |
| 39 | 98% | 48% | 36–61% |
| 40 | 98% | 52% | 39–64% |
| 41 | 98% | 50% | 38–63% |
| 42 | 98% | 53% | 41–66% |
| 43 | 98% | 50% | 38–61% |
| 44 | 98% | 42% | 30–55% |
| 45 | 98% | 44% | 33–56% |
| 46 | 98% | 42% | 30–55% |
| 47 | 98% | 41% | 28–53% |
| 48 | 99% | 42% | 30–55% |
| 49 | 99% | 36% | 25–48% |
| 50 | 100% | 28% | 17–39% |
| 51 | 100% | 28% | 17–39% |
| 52 | 99% | 28% | 17–39% |
| 53 | 99% | 25% | 16–36% |
| 54 | 99% | 20% | 11–31% |
| 55 | 99% | 23% | 13–34% |
| 56 | 99% | 42% | 30–55% |
| 57 | 99% | 59% | 47–72% |
| 58 | 99% | 80% | 70–89% |
| 59 | 98% | 80% | 70–89% |
| 60 | 98% | 84% | 75–92% |
| 61 | 99% | 86% | 77–94% |
| 62 | 99% | 89% | 81–95% |
| 63 | 98% | 94% | 88–98% |
Patching finds where the switch happens. Writing the Java request’s activations into the Apex request’s run at one layer, at the last position only, moves 5% of the answer at layer 32 and 35% at layer 48, then 76% at layer 52 and 89% at layer 56. Before about layer 50 the language is still being read in from the request by later layers; between layers 50 and 54 the decision settles at the position that writes the next token. The other way round, Apex into Java, the last position moves 8% at layer 48 and 80% at layer 52. Swapping all of the code that way gives uneven results in the middle layers, with wide intervals, and isn’t read into here.
- All of the code
- Last position only
Show the numbers
| Layer | Last position only | 95% interval | All of the code | 95% interval |
|---|---|---|---|---|
| 0 | 0% | 0–0% | 0% | 0–0% |
| 4 | 1% | 0–2% | 2% | 1–3% |
| 8 | 2% | 1–3% | 5% | 3–8% |
| 12 | 2% | 1–4% | 7% | 4–10% |
| 16 | 2% | 1–5% | 8% | 5–12% |
| 20 | 3% | 0–7% | 22% | 12–34% |
| 24 | 5% | 1–8% | 19% | 14–29% |
| 28 | 5% | 2–13% | 19% | 13–25% |
| 32 | 5% | 3–12% | 19% | 15–28% |
| 36 | 16% | 10–29% | 54% | 48–58% |
| 40 | 22% | 14–36% | 61% | 55–66% |
| 44 | 29% | 18–49% | 67% | 61–71% |
| 48 | 35% | 25–54% | 70% | 64–74% |
| 50 | 54% | 41–71% | 79% | 75–84% |
| 52 | 76% | 63–86% | 93% | 91–94% |
| 54 | 85% | 77–88% | 94% | 93–95% |
| 56 | 89% | 85–92% | 97% | 95–98% |
| 58 | 92% | 88–95% | 99% | 98–99% |
| 60 | 93% | 89–96% | 99% | 98–100% |
| 62 | 98% | 97–100% | 99% | 99–100% |
| 63 | 100% | 100–100% | 100% | 100–100% |
Steering checks that this is one direction the model uses. The average difference between the Apex and Java requests at layer 50, at the last position, works like a switch. Taken away from the Apex request, it cuts the pairs where the Apex form wins from 60 of 64 to 21; a random direction of the same length leaves 59. Added to the Java request, it gives the Apex form in 15 of 64 pairs, up from 3 (random: 4). Each test leaves the pair it is tested on out of the direction.
Show the numbers
| Asked for | Steering | Pairs where the Apex form wins, of 64 |
|---|---|---|
| Asked for Apex | no steering | 60 |
| Asked for Apex | a random direction of the same length | 59 |
| Asked for Apex | the Apex direction taken away | 21 |
| Asked for Java | no steering | 3 |
| Asked for Java | a random direction of the same length | 4 |
| Asked for Java | the Apex direction added | 15 |
That direction, added or taken away at every position at layer 50, was then tried on 12 Apex requests the model answered in full: tests, utility methods and a trigger handler. The replies were searched for things an Apex compiler rejects or only Java writes, such as System.out, JUnit’s @Test, int, double-quoted strings, ArrayList and import. Unsteered, there were 0. With the direction taken away, the model answered in Java: 10 of 12 replies opened a Java code block, and all 12 had Java-isms, 98 in all. A random direction of the same length left 0.
| Steering at layer 50 | Java-isms found | Replies with any, of 12 | Replies in a Java code block |
|---|---|---|---|
| No steering | 0 | 0 | 0 |
| Apex direction added | 0 | 0 | 0 |
| Apex direction taken away | 98 | 12 | 10 |
| Random direction | 0 | 0 | 0 |
With the direction taken away, 53 of the 98 are double-quoted strings and 19 are JUnit’s @Test.
The opening lines of one reply, to “Write an Apex unit test for a DiscountCalculator.apply(Decimal amount) method that gives 10% off amounts over 1,000. Reply with just the code.”:
@isTest
private class DiscountCalculatorTest {
@isTest
static void testApplyDiscountForAmountOver1000() {
Decimal amount = 1500.00;
Decimal expected = 1350.00;
Decimal actual = DiscountCalculator.apply(amount);
System.assertEquals(expected, actual, 'Discount should be 10% off for amounts over 1000');
}public class DiscountCalculatorTest {
@Test
public void testApplyDiscount() {
// Test case: Amount over 1000 should get 10% discount
double amount = 1500.0;
double expected = amount * 0.9;
double result = DiscountCalculator.apply(amount);
assertEquals(expected, result, "Discount should be 10% for amounts over 1000");
Some Java does reach the answers. In 4 of the 6 assertion pairs, the model’s own next token when asked for Apex is a Java library’s (.assertEquals, .assertFalse, .notNull), which Apex’s Assert class doesn’t have. By kind of idiom:
| Kind of idiom | Pairs | Java token ahead at layer 54 | Final answer, asked for Apex | Final answer, no language named |
|---|---|---|---|---|
| Logging (System.debug / System.out.println) | 8 | 63% | +11.7 (Apex) | −11.6 (Java) |
| String quotes (single / double) | 8 | 88% | +9.9 (Apex) | −12.0 (Java) |
| Collections (List / ArrayList) | 9 | 78% | +12.1 (Apex) | −6.5 (Java) |
| Number types (Integer, Decimal / int, BigDecimal) | 8 | 88% | +4.1 (Apex) | −7.3 (Java) |
| Test annotations (@IsTest / @Test) | 6 | 100% | +8.2 (Apex) | −10.6 (Java) |
| Class modifiers (with sharing / none) | 6 | 100% | +0.8 (Apex) | −9.0 (Java) |
| Assertions (Assert.areEqual / assertEquals) | 6 | 100% | −0.5 (Java) | −1.6 (Java) |
| Exceptions (DmlException / SQLException) | 4 | 75% | +8.9 (Apex) | −3.8 (Java) |
| String equality (== / .equals) | 5 | 0% | +5.6 (Apex) | +0.6 (Apex) |
| Queries (inline SOQL / a repository) | 4 | 100% | +10.2 (Apex) | −3.0 (Java) |
Final answers are the median log-odds at the last layer: above zero the Apex form wins.
02 · Clicks or code
Does it reach for code when clicks would do?
Much Salesforce work can be done two ways: with clicks, the platform’s declarative features such as validation rules, formula fields and Flow, or with code, which on Salesforce means Apex. Salesforce’s guidance is to use clicks where they do the job and code where they don’t. A model that has read far more code than configuration might reach for code anyway.
The study wrote 36 requirements, each with a textbook answer from that guidance: 12 that a standard feature does (a validation rule, a formula or roll-up summary field, an assignment, escalation, duplicate or sharing rule, an approval process), 12 that a flow does, and 12 that need Apex (high volumes, logic across many objects, callouts with retries, a custom endpoint, parsing a file). With reasoning off, the model was asked about each one three ways: how it would build it, in one sentence; the one feature it would use; and, offered both, clicks or code in one word, asked in both orders with the chance of each answer read directly. An answer reaches for code if it names custom code: Apex, a Lightning Web Component, or a flow that calls Apex.
- 95% interval
Show the numbers
| Textbook answer | Asked | Reaches for code | 95% interval |
|---|---|---|---|
| A standard feature does it | how it would build it, in one sentence | 0% | 0–0% |
| A standard feature does it | the one feature it would use | 0% | 0–0% |
| A standard feature does it | offered both: clicks or code, in one word | 3% | 2–5% |
| A flow does it | how it would build it, in one sentence | 8% | 0–25% |
| A flow does it | the one feature it would use | 8% | 0–25% |
| A flow does it | offered both: clicks or code, in one word | 2% | 1–3% |
| It needs Apex | how it would build it, in one sentence | 92% | 75–100% |
| It needs Apex | the one feature it would use | 25% | 0–50% |
| It needs Apex | offered both: clicks or code, in one word | 89% | 77–97% |
Where clicks will do, it almost never reaches for code. Its one-sentence answer names code for 1 of the 24 requirements that a standard feature or a flow can do, and offered both, it gives “code” a 2% chance on average. Where Apex is needed, its one-sentence answer names code for 11 of 12, 3 of them as a flow that calls Apex, which the guidance also allows, and offered both, it gives “code” 89%.
Asked for a single feature, it names code less often, even where code is needed: 3 of 12 times. For those requirements it most often names Platform Events (4 of 12), which carries events to a flow or to Apex rather than doing the work itself.
| Textbook answer | The feature it named (times) |
|---|---|
| A standard feature does it | Validation Rule (3), Formula (2), Formula Field (1), Roll-Up Summary (1), Assignment Rules (1), Duplicate Management (1), Approval Process (1), Territory Management (1), Time-Dependent Workflow (1) |
| A flow does it | Flow (9), Workflow Rule (1), Scheduled Apex (1), Guided Setup (1) |
| It needs Apex | Platform Events (4), Flow (2), Duplicate Management (1), Bulk API (1), Sharing Rules (1), Trigger Handler (1), Scheduled Apex (1), Apex REST (1) |
Asked for the one Salesforce feature it would use, with nobody named: its answers as it wrote them.
- 95% interval
Show the numbers
| Textbook answer | Who asks | Reaches for code | 95% interval |
|---|---|---|---|
| A standard feature does it | nobody named | 0% | 0–0% |
| A standard feature does it | an admin asks | 0% | 0–0% |
| A standard feature does it | a developer asks | 0% | 0–0% |
| A flow does it | nobody named | 8% | 0–25% |
| A flow does it | an admin asks | 0% | 0–0% |
| A flow does it | a developer asks | 33% | 8–59% |
| It needs Apex | nobody named | 92% | 75–100% |
| It needs Apex | an admin asks | 92% | 75–100% |
| It needs Apex | a developer asks | 100% | 100–100% |
Who asks moves the open answer, one way. When a developer asks, its one-sentence answer reaches for code for 4 of the 12 requirements a flow can do, against 1 with nobody named and 0 when an admin asks: +33 points against the admin (95% interval +8 to +58), and not clearly different from nobody named (+25, 0 to +50). Requirements a standard feature does don’t move. Offered a straight choice, the asker barely matters: on requirements that need Apex, an admin asking lowers the chance of “code” by 6 points (95% interval 3 to 9), and a developer asking is not clearly different from nobody named (+1, 0 to +2).
Its answers also show their age. With nobody named, 33% of its one-sentence answers for requirements clicks can do suggest Process Builder or Workflow Rules, often as “a Flow (or Process Builder)”. Salesforce has retired both in favour of Flow.
- A standard feature does it
- A flow does it
- It needs Apex
Show the numbers
| Layer | A standard feature does it | A flow does it | It needs Apex | 95% interval, it needs Apex |
|---|---|---|---|---|
| 0 | +0.4 | +0.4 | +0.4 | 0.4 to 0.4 |
| 1 | +0.7 | +0.7 | +0.7 | 0.7 to 0.7 |
| 2 | +0.9 | +0.9 | +0.9 | 0.9 to 0.9 |
| 3 | +0.9 | +0.9 | +0.9 | 0.9 to 0.9 |
| 4 | +1.0 | +1.0 | +1.1 | 1.0 to 1.1 |
| 5 | +0.8 | +0.8 | +0.8 | 0.8 to 0.9 |
| 6 | +0.5 | +0.5 | +0.5 | 0.5 to 0.5 |
| 7 | +0.6 | +0.6 | +0.6 | 0.6 to 0.7 |
| 8 | +0.7 | +0.7 | +0.7 | 0.7 to 0.8 |
| 9 | +1.0 | +1.0 | +1.0 | 1.0 to 1.1 |
| 10 | +1.0 | +1.0 | +1.0 | 1.0 to 1.1 |
| 11 | +1.1 | +1.1 | +1.2 | 1.2 to 1.2 |
| 12 | +1.2 | +1.2 | +1.3 | 1.2 to 1.3 |
| 13 | +1.2 | +1.2 | +1.3 | 1.2 to 1.4 |
| 14 | +1.1 | +1.1 | +1.2 | 1.2 to 1.3 |
| 15 | +0.5 | +0.6 | +0.6 | 0.5 to 0.6 |
| 16 | +0.0 | +0.1 | +0.1 | 0.0 to 0.1 |
| 17 | −0.4 | −0.4 | −0.4 | −0.4 to −0.4 |
| 18 | −0.1 | −0.1 | −0.0 | −0.1 to −0.0 |
| 19 | −0.6 | −0.6 | −0.7 | −0.7 to −0.6 |
| 20 | +0.1 | +0.1 | +0.1 | 0.1 to 0.1 |
| 21 | −0.5 | −0.5 | −0.5 | −0.5 to −0.5 |
| 22 | −0.3 | −0.3 | −0.4 | −0.5 to −0.4 |
| 23 | −0.3 | −0.3 | −0.4 | −0.4 to −0.3 |
| 24 | −0.8 | −0.7 | −0.7 | −0.8 to −0.7 |
| 25 | −0.9 | −0.9 | −0.8 | −0.9 to −0.7 |
| 26 | −0.1 | −0.1 | −0.0 | −0.1 to 0.0 |
| 27 | +0.4 | +0.4 | +0.5 | 0.4 to 0.6 |
| 28 | −0.4 | −0.4 | −0.4 | −0.5 to −0.4 |
| 29 | −1.2 | −1.3 | −1.2 | −1.2 to −1.1 |
| 30 | −1.2 | −1.2 | −1.0 | −1.1 to −0.9 |
| 31 | −0.2 | −0.1 | −0.1 | −0.1 to 0.1 |
| 32 | +0.7 | +0.7 | +0.8 | 0.7 to 1.0 |
| 33 | +1.4 | +1.6 | +1.8 | 1.7 to 1.9 |
| 34 | +1.9 | +2.1 | +2.3 | 2.1 to 2.4 |
| 35 | +2.1 | +2.2 | +2.5 | 2.3 to 2.5 |
| 36 | +1.9 | +2.1 | +2.4 | 2.3 to 2.5 |
| 37 | +0.4 | +0.5 | +0.8 | 0.7 to 0.9 |
| 38 | +0.2 | +0.3 | +0.3 | 0.3 to 0.4 |
| 39 | +0.3 | +0.3 | +0.3 | 0.3 to 0.4 |
| 40 | +0.4 | +0.5 | +0.5 | 0.4 to 0.5 |
| 41 | +0.2 | +0.3 | +0.2 | 0.1 to 0.3 |
| 42 | +0.1 | +0.1 | +0.0 | −0.0 to 0.1 |
| 43 | +0.0 | +0.1 | −0.0 | −0.1 to 0.1 |
| 44 | −0.2 | −0.3 | −0.2 | −0.3 to −0.1 |
| 45 | −0.5 | −0.5 | −0.5 | −0.6 to −0.4 |
| 46 | −0.0 | −0.1 | −0.1 | −0.2 to 0.1 |
| 47 | −0.0 | −0.0 | +0.5 | 0.4 to 0.6 |
| 48 | −0.8 | −0.7 | −0.1 | −0.2 to −0.0 |
| 49 | −0.9 | −0.9 | −0.3 | −0.4 to −0.2 |
| 50 | +0.2 | +0.4 | +1.2 | 1.1 to 1.2 |
| 51 | +0.3 | +0.5 | +1.3 | 1.1 to 1.3 |
| 52 | −0.2 | +0.2 | +0.9 | 0.8 to 1.1 |
| 53 | −0.6 | −0.3 | +0.3 | 0.2 to 0.4 |
| 54 | −0.5 | −0.2 | +0.4 | 0.2 to 0.5 |
| 55 | +0.2 | −0.5 | +5.5 | 4.5 to 6.6 |
| 56 | −0.5 | −1.1 | +4.5 | 3.8 to 5.6 |
| 57 | −0.9 | −1.5 | +4.5 | 3.8 to 5.5 |
| 58 | −2.2 | −2.7 | +3.9 | 3.2 to 4.7 |
| 59 | −8.7 | −9.5 | +6.5 | 5.3 to 8.0 |
| 60 | −6.1 | −7.3 | +8.8 | 7.0 to 10.3 |
| 61 | −7.6 | −8.7 | +7.2 | 5.3 to 8.8 |
| 62 | −6.0 | −7.1 | +6.2 | 4.6 to 7.6 |
| 63 | −3.9 | −4.6 | +3.3 | 2.2 to 4.4 |
Inside, there is no hidden pull towards code that the model then overrules. Read through the logit lens, the one-word choice looks much the same for all three kinds of requirement up to layer 46, never more than 0.53 log-odds apart (at layer 36): the middle layers’ larger swings are shared by every requirement alike. The one steady difference is a slight lean in the textbook order from layer 30 to 37, with the requirements that need Apex a little nearer code than the flow ones, and those a little nearer than the standard ones. From layer 47 the requirements that need Apex pull ahead, by several log-odds from layer 55, and the others settle firmly on clicks only from layer 58. The choice is made late, like the Apex form in study 1.
03 · The task at hand
Does it know it’ll fail?
Forcebench graded this model, in these weights, on the 272 public tasks of benchmark 0.1.0, with reasoning effort low: it passed 78 (29%; the leaderboard’s suite-averaged score is 28%). Each task was shown to it again exactly as in that run, and its activations were read at the end of the prompt, before it had started reasoning. Separately, with reasoning off, it was shown each task with a question added at the end that told it not to solve the task yet: how likely its answer would be to pass, answered with one digit from 0 to 9.
Each signal is scored by its AUROC: the chance that a task it passed ranks above a task it failed, where 0.5 is a coin toss and 1 is perfect.
- 95% interval
- A coin toss (0.5)
Show the numbers
| Signal | AUROC | 95% interval | Within a suite |
|---|---|---|---|
| A probe on its activations, before it starts | 0.81 | 0.76–0.86 | 0.70 |
| The suite, difficulty and prompt length | 0.75 | 0.69–0.81 | 0.59 |
| What it says: its chance of passing | 0.64 | 0.56–0.71 | 0.61 |
Before it has written a word, a linear probe on its activations at layer 40, a layer fixed before looking, tells the tasks it will pass from the ones it will fail with an AUROC of 0.81 (95% interval 0.76–0.86); the best layer, 46, reaches 0.83. With the labels shuffled, the best of all 64 layers gets past 0.62 only one time in twenty.
Some of that is just the kind of task. The suite, the difficulty and the prompt’s length alone give 0.75. The probe adds to that (+0.07, 95% interval +0.03 to +0.10), and it still reaches 0.70 comparing tasks within the same suite, where the baseline manages 0.59. Reading its own finished answer doesn’t help: a probe at the end of the answer does about as well (0.78 at layer 40).
Each label is one graded attempt at temperature 1.0, so some passes and fails are luck. As a check, 6 other runs of the same model in AWQ int4, at low, medium and xhigh effort, give each task a pass rate over at least 4 attempts. That pass rate predicts this run’s results with an AUROC of 0.89, about as well as anything could, and a probe trained on that pass rate predicts it with a rank correlation of 0.59 (the suite baseline: 0.40).
- Before it starts
- After its own answer
- Suite, difficulty, length
- Shuffled labels (best layer, 95th percentile)
Show the numbers
| Layer | Before it starts | 95% interval | After its own answer | 95% interval |
|---|---|---|---|---|
| 0 | 0.67 | 0.60–0.74 | 0.70 | 0.62–0.76 |
| 1 | 0.75 | 0.68–0.81 | 0.72 | 0.66–0.78 |
| 2 | 0.76 | 0.70–0.82 | 0.73 | 0.66–0.80 |
| 3 | 0.72 | 0.64–0.78 | 0.73 | 0.66–0.80 |
| 4 | 0.71 | 0.64–0.78 | 0.73 | 0.66–0.79 |
| 5 | 0.71 | 0.65–0.79 | 0.74 | 0.68–0.81 |
| 6 | 0.72 | 0.66–0.78 | 0.76 | 0.70–0.83 |
| 7 | 0.73 | 0.66–0.79 | 0.76 | 0.69–0.81 |
| 8 | 0.77 | 0.70–0.83 | 0.78 | 0.72–0.83 |
| 9 | 0.78 | 0.72–0.83 | 0.76 | 0.70–0.82 |
| 10 | 0.79 | 0.74–0.85 | 0.75 | 0.69–0.82 |
| 11 | 0.77 | 0.70–0.83 | 0.74 | 0.68–0.81 |
| 12 | 0.75 | 0.69–0.81 | 0.76 | 0.70–0.82 |
| 13 | 0.78 | 0.72–0.83 | 0.75 | 0.69–0.81 |
| 14 | 0.78 | 0.72–0.84 | 0.75 | 0.69–0.81 |
| 15 | 0.77 | 0.71–0.83 | 0.75 | 0.69–0.81 |
| 16 | 0.77 | 0.71–0.82 | 0.75 | 0.69–0.82 |
| 17 | 0.81 | 0.75–0.86 | 0.76 | 0.70–0.82 |
| 18 | 0.82 | 0.77–0.87 | 0.75 | 0.68–0.81 |
| 19 | 0.81 | 0.74–0.86 | 0.74 | 0.67–0.80 |
| 20 | 0.80 | 0.75–0.86 | 0.74 | 0.67–0.80 |
| 21 | 0.80 | 0.74–0.84 | 0.74 | 0.68–0.81 |
| 22 | 0.79 | 0.73–0.84 | 0.74 | 0.68–0.81 |
| 23 | 0.79 | 0.73–0.85 | 0.75 | 0.69–0.81 |
| 24 | 0.79 | 0.73–0.85 | 0.75 | 0.68–0.81 |
| 25 | 0.79 | 0.73–0.84 | 0.75 | 0.69–0.82 |
| 26 | 0.77 | 0.70–0.83 | 0.76 | 0.69–0.82 |
| 27 | 0.79 | 0.73–0.84 | 0.75 | 0.69–0.82 |
| 28 | 0.77 | 0.71–0.83 | 0.75 | 0.69–0.82 |
| 29 | 0.78 | 0.72–0.84 | 0.76 | 0.70–0.82 |
| 30 | 0.79 | 0.73–0.84 | 0.75 | 0.69–0.82 |
| 31 | 0.79 | 0.72–0.84 | 0.77 | 0.72–0.83 |
| 32 | 0.80 | 0.75–0.86 | 0.76 | 0.71–0.82 |
| 33 | 0.82 | 0.77–0.87 | 0.78 | 0.72–0.84 |
| 34 | 0.83 | 0.77–0.87 | 0.78 | 0.72–0.84 |
| 35 | 0.82 | 0.76–0.87 | 0.78 | 0.72–0.83 |
| 36 | 0.81 | 0.75–0.86 | 0.78 | 0.73–0.84 |
| 37 | 0.81 | 0.76–0.86 | 0.79 | 0.73–0.84 |
| 38 | 0.82 | 0.77–0.87 | 0.76 | 0.71–0.82 |
| 39 | 0.81 | 0.75–0.86 | 0.77 | 0.70–0.82 |
| 40 | 0.81 | 0.75–0.86 | 0.78 | 0.73–0.84 |
| 41 | 0.81 | 0.75–0.86 | 0.77 | 0.71–0.83 |
| 42 | 0.81 | 0.75–0.86 | 0.77 | 0.72–0.84 |
| 43 | 0.81 | 0.76–0.86 | 0.78 | 0.72–0.83 |
| 44 | 0.82 | 0.76–0.86 | 0.79 | 0.74–0.85 |
| 45 | 0.82 | 0.77–0.87 | 0.79 | 0.73–0.85 |
| 46 | 0.83 | 0.78–0.88 | 0.78 | 0.72–0.83 |
| 47 | 0.82 | 0.77–0.88 | 0.80 | 0.74–0.86 |
| 48 | 0.82 | 0.76–0.87 | 0.80 | 0.74–0.85 |
| 49 | 0.82 | 0.77–0.87 | 0.81 | 0.75–0.86 |
| 50 | 0.82 | 0.77–0.87 | 0.81 | 0.75–0.86 |
| 51 | 0.82 | 0.76–0.87 | 0.80 | 0.75–0.86 |
| 52 | 0.81 | 0.75–0.86 | 0.81 | 0.76–0.87 |
| 53 | 0.82 | 0.76–0.87 | 0.81 | 0.74–0.86 |
| 54 | 0.82 | 0.76–0.87 | 0.81 | 0.75–0.86 |
| 55 | 0.81 | 0.76–0.87 | 0.81 | 0.75–0.87 |
| 56 | 0.82 | 0.77–0.87 | 0.81 | 0.76–0.86 |
| 57 | 0.82 | 0.76–0.87 | 0.80 | 0.75–0.86 |
| 58 | 0.82 | 0.77–0.87 | 0.81 | 0.74–0.86 |
| 59 | 0.82 | 0.76–0.87 | 0.80 | 0.74–0.86 |
| 60 | 0.82 | 0.76–0.87 | 0.81 | 0.76–0.86 |
| 61 | 0.82 | 0.77–0.87 | 0.80 | 0.73–0.86 |
| 62 | 0.83 | 0.77–0.88 | 0.80 | 0.74–0.85 |
| 63 | 0.82 | 0.76–0.88 | 0.80 | 0.74–0.86 |
What it says is another matter. Asked how likely its answer is to pass, it gives 7.6 out of 9 on average (7.9 for tasks it went on to pass, 7.5 for ones it failed), against a pass rate of 29%. Its stated confidence separates passes from fails with an AUROC of 0.64, below the suite baseline (−0.12, 95% interval −0.19 to −0.04); the probe beats it by +0.18 (+0.10 to +0.26). Only its most confident sixth of tasks pass noticeably more often.
Two things tilt that comparison against what it says. It answered with reasoning off, while the graded answers were written with reasoning on. And the probe, like the baseline, was fitted to these grades (scored only on tasks held out of the fit), while its stated confidence was never fitted to anything. So the fair yardstick for the probe is the baseline. What stands either way: the model rates its chances far above how often it passes, and its rating tells passes from fails less well than the kind of task does.
- 95% interval
- The pass rate it implied
Show the numbers
| Stated confidence (of 9) | Tasks | Passed | 95% interval | The pass rate it implied |
|---|---|---|---|---|
| 6.1 | 46 | 17% | 7–30% | 68% |
| 7.2 | 46 | 30% | 17–43% | 80% |
| 7.7 | 45 | 20% | 9–33% | 85% |
| 8.0 | 45 | 24% | 13–38% | 89% |
| 8.3 | 45 | 18% | 7–29% | 92% |
| 8.7 | 45 | 62% | 47–76% | 96% |
| Suite | Tasks | Passed | Stated chance of passing (0–9) | 95% interval |
|---|---|---|---|---|
| SOQL | 20 | 55% | 8.2 | 7.9 to 8.5 |
| Governor limits | 19 | 47% | 7.5 | 7.1 to 7.9 |
| Salesforce APIs | 18 | 44% | 8.1 | 7.8 to 8.3 |
| Trigger Actions Framework | 18 | 44% | 7.1 | 6.7 to 7.6 |
| Permissions | 15 | 40% | 7.6 | 7.2 to 8.1 |
| Apex | 20 | 35% | 7.9 | 7.6 to 8.2 |
| NPSP | 18 | 33% | 7.4 | 6.9 to 7.8 |
| Salesforce CLI | 20 | 30% | 8.3 | 8.1 to 8.4 |
| CI/CD | 17 | 29% | 8.0 | 7.8 to 8.2 |
| Lightning Web Components | 18 | 22% | 8.2 | 7.9 to 8.4 |
| Packaging | 18 | 22% | 7.5 | 7.3 to 7.7 |
| Flow | 18 | 17% | 7.2 | 6.8 to 7.6 |
| Scratch org definitions | 18 | 6% | 7.1 | 6.8 to 7.4 |
| Platform docs | 17 | 0% | 6.3 | 5.9 to 6.7 |
| fflib | 18 | 0% | 8.0 | 7.8 to 8.2 |
Graded run: 20260927T004832Z_qwen3.8-27b-mlx-8bit@low. The stated chance is the expected digit from the model’s probabilities for 0 to 9, asked as “Don't solve it yet. How likely is it that your answer would pass the grader's hidden tests? Reply with one digit, 0 (certain to fail) to 9 (certain to pass).”
What this doesn’t show
- It’s one model, in one quantisation. Another model, or this one at full precision, may draw its lines elsewhere.
- The code pairs and requirements were written for this study, and the textbook answers are its reading of Salesforce’s guidance: a few have more than one fair answer. They are small samples: 64 code pairs, and 12, 12 and 12 requirements. Study 3 uses the 272 public tasks of benchmark 0.1.0, the version its graded run used.
- Each open answer in study 2 is one reply with reasoning off, the model’s most likely words, sorted by the features it names. With reasoning on, or sampled, it may answer differently.
- The logit lens is approximate in the middle layers, which weren’t trained to be read this way. The probe and patching results don’t depend on it.
- The pass or fail of each task is one attempt, and a probe that predicts it is a correlation: it shows the information is there before the model starts, not that the model uses it.
- The after-answer read sees the model’s final answer but not the reasoning before it, which the study left out.
Numbers from the study file generated 22 October 2026. Code, stimuli and per-item results: github.com/forcebench-ai/forcebench-mri.
Private evaluations
Want this for your team?
Azul Labs runs Forcebench privately for two kinds of Salesforce team.
Choosing a model to run yourself?
A private benchmark of your shortlist
Open-weight models at the quantisations and reasoning efforts you could run on infrastructure you control, tested on your kind of Salesforce work, with a written recommendation.
- Private tasks written from your work only ever run on local models; they are never sent to an API.
- Re-testing after any change of model, quantisation or engine is available.
- Typical turnaround is 2–3 weeks; large models take about a day of machine time per configuration.
Building or buying a Salesforce AI tool?
A private evaluation of your tool
Your tool on held-out tasks it has never seen, against API baselines (models reached through a hosted service) and Local ones (served on Forcebench’s own hardware), with a written report on where it holds up and where it fails.
- It can start as a smaller pilot, about 30 held-out tasks with your tool against two baselines, before a full evaluation.
- Repeat evaluations after your releases are available.
- Typical turnaround is 2–3 weeks.
Fixed fee, quoted up front. The quote depends on the number of tasks and the number of models, tools or configurations tested.
Or email leo@azl.au · azl.au