Monday, 10 August 2026

Transactions in Apex Integration Testing

Image generated by OpenAI GPT-5.5 from a prompt by Bob Buzzard

Introduction

In the first post in this series I covered a brief overview of the new Apex Integration Testing that is available in Developer Preview in Summer '26. In this instalment I'll take a deeper look at the how transactions work in this new testing paradigm. If you want to play along at home, you can find code samples to verify the behaviour at this Github repository. It might seem a little odd that I'm asserting behaviour rather than simply logging it etc, but I find this to be a useful approach when the implementation is likely to change. Come Winter '27 I'll just execute my tests again and if any fail I'll know something has changed. Tests as a tripwire FTW!

As always, please bear in mind that this functionality is in Developer Preview and the implementation could well change, rendering any findings in this post null and void. It's good fun to figure this stuff out though, so let's have at it.

Integration Tests Execute in a Single Transaction

This is probably the key takeaway of the current implementation. Just because records can be persisted to the Salesforce database as part of an integration test, the transaction doesn't end at that point. The docs for the new IntegrationTest.commitTestOnly() method make this abundantly clear:

Commits data to the database mid-transaction so it’s visible to service threads such as Agentforce and Data 360. It resets the uncommitted work checkpoint and mixed DML tracking for the new transaction boundary. 

The key word(s) here being mid-transaction - we are only part way through, and the transaction will continue for the rest of the test. Sadly this also means that DML limits are not reset by the mid-transaction commit, so we still have to squeeze all of our testing into a single limits context, although like unit tests we do still have the new limits context available through Test.startTest() once we've set up all our data. (See LimitUsageIntegrationTest.cls)

The other interesting part of the docs is the resetting of mixed DML tracking - I foresee that once this is GA we'll all be moving unit tests that have to workaround this particular issue over to integration tests. 

SavePoints Do Not Survive Mid-Transaction Commits

(Thanks to Steve Fouracre for the question about SavePoints - make sure to sign up for our AI events series).

As the transaction continues after a mid-point commit, you could be forgiven for thinking that any SavePoints created earlier in the transaction are still available for use, but sadly (again) that isn't the case. 

Attempting to use a SavePoint (rolling back or releasing) after a commit gives the following error:

18:34:22:291 EXCEPTION_THROWN [15]|System.TypeException:
                                                              Savepoint does not exist in this context

(See SavePointIntegrationTest.cls)

Integration Test Transactions Autocommit

If you make changes to the database but don't carry out a mid-transaction commit, those changes will be auto-committed when the test completes. In this respect an integration test is just like any other Salesforce request. You can see proof of this in AutoCommitIntegrationTest.cls, and note that I'm verifying this through an assertion in the @TearDown method:

@TearDown
static void tearDown() {    
    List<Account> accs=[SELECT Id, Name 
                        FROM Account
                        WHERE Name LIKE 'Integration Test Account%'
                        WITH USER_MODE]; Assert.areEqual(2, accs.size(),
               'Expected to find mid-transaction and auto committed test accounts'); delete as user accs; }

while certainly unusual, there's nothing prohibited about using asserts outside of test methods, and in this case it's the only way I can be sure I'm outside of the integration test transaction boundary. I'd steer clear of it in production tests though, as it will likely cause confusion.

TearDown Executes in a New Transaction

When the @TearDown method executes, it does so in a new transaction context and thus has a full set of governor limits to use to cleanup data. The teardown method autocommits, so as long as it completes successfully your cleanup changes will be persisted. It's worth point out again that teardown methods can error, so you need to use defensive coding to ensure you expect the unexpected. 

Talking of unexpected, it also appears that if you have multiple @TearDown methods, they each get their own transaction In TearDownTestTXIntegrationTest.cls I have two of these and in each I assert that the DML statements consumed is 0, which succeeds even though one of them has to have already executed and handled the delete:

@TearDown
static void tearDown() {
    Assert.areEqual(0, Limits.getDMLStatements(), 'Should be 0 DML statements consumed in teardown');
    delete as user [SELECT Id FROM Account WHERE Name = 'Integration Test Account' WITH USER_MODE];
}

@TearDown
static void tearDown2() {
    Assert.areEqual(0, Limits.getDMLStatements(), 'Should be 0 DML statements consumed in teardown');
    delete as user [SELECT Id FROM Account WHERE Name = 'Integration Test Account' WITH USER_MODE];
}

More Information


Follow on LinkedIn


Wednesday, 29 July 2026

Apex Integration Testing in Summer '26

Image eventually created by OpenAI GPT-5.5 after
about 5 prompts by Bob Buzzard

Introduction

A feature that seems to have flown under the radar in the Summer '26 release of Salesforce is the new Apex Integration Testing capability. For the first time integration testing (albeit quite limited) is a first-class discipline on the platform. With a bit of luck this foreshadows an end to our reliance on external tooling and Heath Robinson (or Rube Goldberg for our friends across the pond) combinations of unit tests and mocks masquerading as integration tests. 

Before we get too excited it's worth bearing in mind that this is currently in Developer Preview for scratch orgs only, and is limited to interactions with the Data 360 and Agentforce Salesforce services, but hopefully this will be relaxed in the future and we'll be able to make callouts to any authorised endpoint that we choose. 

If you'd like to know more about Apex integration testing, I've covered it in depth (as much as possible for a developer preview feature, obviously) in my new book: Software Testing on the Salesforce Platform

Integration Tests

If you haven't come across integration testing before, you can think of it as the next programmatic testing step after unit testing. Unit tests check whether a piece of code works in total isolation, while integration tests check that the various unit tested pieces still work correctly when they are connected to each other. 

An Apex class/method becomes an integration test through the simple application of the @IntegrationTest annotation where a unit test would have the @IsTest annotation. Aside from that, they start out rather similar to Apex unit tests: 

  • Set up data 
  • Execute code under test
  • Verify results.
Where they diverge (but align with just about every other unit test framework in the world) is tearing down the changes made. In Salesforce world we've never had to care about that before, as the Apex unit test framework rolls back any changes we've made automatically. Integration tests commit changes to the Salesforce database, so there's clearly a need to reset the database to a known, stable state. 

Teardown methods are identified through the @TearDown annotation, are executed after every test regardless of success or failure, and commit automatically. You can also have more than one of them in a class, in which case they will all execute after every test and you can't specify the order that they execute in. 

It's also really important to bear in mind that just because teardown methods run after every test, they won't necessarily complete successfully. They can error, just like any other piece of code. If they do error, the test will be marked as a failure, as well as the database being left in an indeterminate state. For this reason, defensive code is a must for teardown methods. 

Executing Integration Tests

As integration testing is in developer preview, it can only be used in scratch orgs at present. Enable integration testing through the ApexIntegrationTests feature:


Once you have created a scratch org with this configuration, you can deploy integration test classes to it. You can execute integration test classes like unit test classes, through setup, the developer console or the Salesforce CLI, although remember they have to run asynchronously.

You can only have a single integration test running in an org at any point in time. This isn't an issue in a scratch org, but could be entertaining in sandboxes - everyone gets a couple of hours a week to run tests in peace maybe? Although I'm sure that restriction will be loosened as the functionality approaches GA. 

Apex governor limits apply to integration tests, which probably restricts how useful they will actually be. This is another aspect I wouldn't be surprised to see loosened over time, they are running asynchronously after all, but it's unlikely we'll get to parity with external tools that can span multiple transactions. 

More Information

Follow on LinkedIn


Monday, 19 January 2026

Run Relevant Tests Annotations in Spring '26

Image created by ChatGPT5.2 based on a prompt by Bob Buzzard

Introduction


In my first post on this new functionality I did a bit of digging into which tests are chosen when code is changed. If leaving it up to Salesforce doesn't quite cut it, there's the option to influence things via the two new parameters for the @isTest annotation. 

As in the first post, I've got a small set of classes with dependencies that are sometimes static and sometimes dynamic. As a refresher the key classes are:
  • OpportunityUtils
    The protagonist in my little drama is a class named  This implements an interface (OpportunityUtilsIF) with a single method, getBigDeals(), which receives a collection of opportunities and returns a new collection containing just those opportunities with a value greater than or equal to 250,000.

    There is a dedicated test class (OpportunityUtilsTest) which directly instantiates the class and executes a zero/one test. 
  • OpportunityEOD
    This contains a single method (EODProcessing) that extracts all opportunities created today, creates an instance of OpportunityUtils, extracts just the big deals, appends ' - BIG DEAL' to their name and updates them.

    There is a dedicated test class (OpportunityEODTest) that inserts test opportunities of varying values, instantiates OpportunityEOD and executes the EODProcessing method, then extracts all opportunities from the database that are greater than or equal to 250,000 and asserts each name contains ' - BIG DEAL'.

  • OpportunityWrapLevel1
    This contains a single method (EODProcessingWrapLevel1) that instantiates the OpportunityEOD class and executes the EODProcessing method.

    There is a dedicated test class (OpportunityWrapLevel1Test) that inserts test opportunities of varying levels, instantiates OpportunityEODWrapLevel1, executes the EODProcessingWrapLevel1 method, then extracts all opportunities from the database that are greater than or equal to 250,000 and asserts each name contains ' - BIG DEAL'.

  • OpportunityEODInjection
    This  contains a replica of the EODProcessing() method, but rather than directly instantiating OpportunityUtils it is passed a parameter (implementing the OpportunityUtilsIF interface. There is a dedicated test class (OpportunityEODInjectionTest) that delegates to a test factory to dynamically create an instance of OpportunityUtils based on the class name - at no point is OpportunityUtils directly referred to. The test mirrors the other EOD tests, inserting opportunities, carrying out the EOD processing and verifying that ' - BIG DEAL' is appended where appropriate.

As we are now in the scratch org preview window, I was able to use my pre-release developer edition as a Dev Hub and create a Spring '26 scratch org, which speeds things up enormously and lets me scrap everything and start again from scratch with minimum effort.

@IsTest(critical=true)


This parameter tells the Salesforce test engine that the tests in this class must execute when a deployment takes place. I originally misread the docs on this and thought the test only executed if the payload contained Apex changes, but that isn't the case. Setting a test class as critical means it always executes for a deployment that runs relevant tests regardless of what is being deployed. To confirm this, I returned to the classic example of a configuration change that breaks tests with ease - the validation rule!

I marked my OpportunityEODTest class as critical, attempted to deploy an Opportunity validation rule that required one of Description or Lead Source to be populated, and duly watched the test execute and the deployment fail! 

This is a great addition, as the critical tests are identified without the deployer having to remember which tests they should always run. That said the deployer does still have to remember to deploy with the RunRelevantTests option, so it's not a silver bullet.

@IsTest(testFor='<classes_and_triggers>')


This parameter comes in handy if the Salesforce test engine isn't picking up all the tests that matter. In my first post I explained how it isn't really reasonable to expect the test engine to pick up dynamic instantiation of the OpportunityUtils class based on its name, which is how it's used in OpportunityEODInjection. Using the testFor parameter I can give the test engine the helping hand it needs:
    @isTest(testFor='ApexClass:OpportunityUtils')
    private class OpportunityEODInjectionTest {
        @isTest
        static void TestEODProcessing() {
           ...
        }
    }
  
Note that I don't have to include OpportunityEODInjection in the list of classes identified in the testFor parameter. I'm adding to the tests that are executed, not overriding them. This test is also executed if I change the OpportunityEODInjection class, as it has a direct dependency on it which the Salesforce test engine can pick up.

Even though that is the case, I think in a real-world environment I'd prefer to list all the classes/triggers that the test class is associated with, as it improves clarity and saves a developer having to figure things out manually, even if there is overhead to create and maintain this information. 

More Information


Follow on LinkedIn



Tuesday, 13 January 2026

Run Relevant Tests in Spring '26

Image created by GPT5.2 based on a prompt by Bob Buzzard

Introduction


The Spring '26 release of Salesforce introduces a new way to execute tests when deploying - Run Relevant Tests. Specifying this option essentially hands over responsibility to Salesforce to identify and execute the tests associated with any Apex code in the deployment payload. Note that this is in beta in Spring '26, and everything following was based on trying it out in a pre-release org in the first half of January 2026, so really early days for it. 

This is the archetypal double-edged sword in my view. One the one hand, deployments can run way faster with little or no human effort, especially compared to manually specifying the tests that should be executed. On the other hand, it's abdicating responsibility for the quality of the deployment. Given that we're already looking at abdicating responsibility for the development of software to AI tools, does ceding another aspect of the lifecycle really matter? 

Who Chooses the Tests?


While conceptually this is something that should require many sleepless nights and lengthy discussions, I think in the majority of cases, especially outside of ISVs, it really doesn't matter. Yes, the test engine might not get it 100% right 100% of the time, but neither will most developers. If we're being honest the test suite itself is likely sub-optimal, especially in mature orgs at the Enterprise level where lots of disconnected parties have focused on getting things live over the years. In this scenario, if the odd test gets missed or an extra test gets run, it doesn't really change much from the quality perspective. 

This is not the case for ISVs though, who tend to put a lot of effort into designing a robust test suite, given that they need their solution to function under pretty much any scenario. Likely also not true for recent orgs that have been following good DevOps and Quality Assurance principles from the start, given that Salesforce and third-party tooling now makes this relatively straightforward to achieve. In these cases, the two new parameters for the @IsTest annotation allow tight coupling of tests to classes/deployments. Note that these only apply when the test level for the deployment is RunRelevantTests:

  • @IsTest(critical=true)

    I really like this one. If you've ever built an app that works with real money, you'll know that there are areas that must not fail or losses will be incurred. Executing tests for key areas, regardless of what changed, is a nice new feature.

  • @IsTest(testFor='<classes_and_triggers>')

    This is for the well-managed codebases. It allows you to guarantee that this test class will be executed if new/modified versions any of the identified dependencies are included in the payload. While this might feel like development overhead, my view is it's exactly what is needed in a robust test suite. Good development teams will likely hold this information elsewhere anyway, and apply it via RunSpecifiedTests, so I can see in many cases it will short cut that process.

How Does Salesforce Choose?


This is the $64,000 question, but right now we just don't know. The docs say :
the RunRelevantTests test engine analyzes the deployment payload and automatically runs a subset of tests based on that analysis.
which tells us what happens, but no detail about the analysis carried out. This isn't unusual in my experience, and by the time this feature goes GA I'd expect significantly more information to be available. That said I can't just sit idly and wait, so I've been doing some digging using a sample codebase with the following actors:
  • OpportunityUtils
    The protagonist in my little drama is a class named  This implements an interface (OpportunityUtilsIF) with a single method, getBigDeals(), which receives a collection of opportunities and returns a new collection containing just those opportunities with a value greater than or equal to 250,000.

    There is a dedicated test class (OpportunityUtilsTest) which directly instantiates the class and executes a zero/one test. 
  • OpportunityEOD
    This contains a single method (EODProcessing) that extracts all opportunities created today, creates an instance of OpportunityUtils, extracts just the big deals, appends ' - BIG DEAL' to their name and updates them.

    There is a dedicated test class (OpportunityEODTest) that inserts test opportunities of varying values, instantiates OpportunityEOD and executes the EODProcessing method, then extracts all opportunities from the database that are greater than or equal to 250,000 and asserts each name contains ' - BIG DEAL'.

  • OpportunityWrapLevel1
    This contains a single method (EODProcessingWrapLevel1) that instantiates the OpportunityEOD class and executes the EODProcessing method.

    There is a dedicated test class (OpportunityWrapLevel1) that inserts test opportunities of varying levels, instantiates OpportunityEODWrapLevel1, executes the EODProcessingWrapLevel1 method, then extracts all opportunities from the database that are greater than or equal to 250,000 and asserts each name contains ' - BIG DEAL'.

  • OpportunityEODInjection
    This ups the ante somewhat, is it contains a replica of the EODProcessing() method, but rather than directly instantiating OpportunityUtils it is passed a parameter (implementing the OpportunityUtilsIF interface. There is a dedicated test class (OpportunityEODInjectionTest) that delegates to a test factory to dynamically create an instance of OpportunityUtils based on the class name - at no point is OpportunityUtils directly referred to. The test mirrors the other EOD tests, inserting opportunities, carrying out the EOD processing and verifying that ' - BIG DEAL' is appended where appropriate.

After deploying these to a Spring '26 pre-release developer edition, I then changed the OpportunityUtils code to consider opportunities with a value of 300,000 and over as big deals, then tried to deploy it using the new -l RunRelevantTests Salesforce CLI option. There were unit test failures, but which ones?
  • OpportunityUtilsTest
    This test class was chosen, and the test for a single big deal failed. All as expected.

  • OpportunityEODTest
    This test class was chosen, and the EOD processing test failed. All as expected.

  • OpportunityWrapLevel1Test
    This test class was not chosen. I was surprised at this, as there is a dependency chain leading to OpportunityUtils.
    In case the selection of the OpportunityEODTest skewed the results, I removed the tests from that class and re-ran from the start. It still wasn't chosen, which suggests to me that currently only tests for classes with direct dependencies on the changed Apex will be chosen. 

  • OpportunityEODInjectionTest
    This test class was not chosen. This does not surprise me at all, as it would be really hard to pick up dynamically instantiated instances. The example I've given is straightforward, but the name could be generated by combining strings, through a lookup collection, or even configuration, so the only way to tell is to actually run the real code. I think this is a scenario where it would be up to the developer to ensure that this code was tested when the dependencies changed via the @IsTest(testFor='...') annotation.

I don't think this is too bad at all. I'd have liked the tests from the dependency chain to be picked up too, but I can see that is a bit of a balancing act. If every class that can possibly reach the changed code is executed, you could end up executing all of your tests every time. Taking the opposing view, if classes that could be impacted by a change aren't tested, what does that mean for our confidence in the deployed code? In this case I'm assuming good intentions, given this is in beta, and expecting this to be tightened up as the functionality makes its way towards general availability.

Conclusion


I like this new feature, but it requires careful consideration before relying on it for production (which, of course, you can't as it's in beta!). Personally I like to execute all tests whenever I deploy, but that isn't always feasible, especially if there is a large set of tests that have to execute serially and a high frequency release cadence. In that scenario I'd likely use the @IsTest(testFor='...') annotation approach to retain tight control. If, however, I was working in a mature org that showed clear signs of the big ball of mud anti-pattern, I'd happily leave it up to Salesforce.

Oh, and the UI hasn't quite caught up with this new functionality, as the deployment status always says that tests weren't required even if some of them failed:


so if you want to know which tests were actually picked, you need to use the Salesforce CLI with the --json flag and parse the output.

If you are interested in learning more about Apex testing, check out my in-progress book Software Testing on the Salesforce Platform.

More Information

Wednesday, 5 November 2025

Screen Flow LWC Local Actions in Winter '26


Original image created by GPT 5o based on a prompt from Bob Buzzard

Introduction

The Winter '26 release of Salesforce introduced Lightning Web Components as local actions in screen flows. This allows client-side JavaScript to be executed as a 'function' in the flow, without the need for a round trip to the server for an Apex action. 

Sample Actions

For this blog post I created a couple of LWCs to act as local actions :

  • A modal that displays a warning/reminder to a user and make them think about what they were about to do
  • A toast that displays the results of the user's request
In order to use an LWC as a local action, it must implement a function named invoke, which carries out the JavaScript processing. The flow runtime executes this function when it encounters a local action element tied to an LWC. The invoke method for my modal local action is shown below:

  @api title;
  @api size;
  @api content;

  @api async invoke() {
  const result=await ModalDemo.open({
            size: this.size,
            title: this.title,
            content: this.content
        });
  console.log(result);
}
This opens the modal and displays a message based on the title, size and content properties supplied by the flow, making it suitable for reuse across multiple scenarios. In order to allow a flow to pass properties to an LWC, you need to decorate the property with @api in the LWC and define it in the targetConfigs section of the js-meta.xml file:
   <targets>
        <target>lightning__FlowAction</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__FlowAction">
            <property name="title" type="String" label="Modal title" role="inputOnly" />
            <property name="content" type="String" label="Modal content" role="inputOnly" />
            <property name="size" type="String" 
                      label="Modal size" default="large" role="inputOnly" />    
        </targetConfig>
    </targetConfigs>

Note that I've defined these with a role of inputOnly, as the component only uses these properties to display the modal.

The Flow


I have simple flow based on the scenario of deleting a contact:



The Warning Modal configuration supplies the property values for the modal for this scenario. 



These properties are simple text values here, but any resource can be used, as this the case for the Success Toast action which uses the contactInformation formula resource:


which is constructed from the record detail of the contact to be deleted:



Executing the Flow


For demo purposes I've added the flow to a Lightning App Builder page - note that it doesn't delete anything, just claims to have done so!



A Gotcha


Fun fact - my original idea for this demo was to have a confirmation dialog asking if you were sure you wanted to delete the record. I had to pivot from this, as it appears that it is currently not possible to pass information back to the flow from the LWC. 

This would typically be achieved by publishing a FlowAttributeChangeEvent to update a property indicating if the user has confirmed or not, but when I try this (even when that is all the invoke method does!) I get the following error from the LWC:


I'm not sure whether this is me (I don't think so, as I've confirmed I'm doing the right thing based on multiple blogs and articles), unsupported (I can't find anything in the docs), or a bug, but it does rather limit the usefulness of this feature so hopefully it's either my mistake or Salesforce sort it out soon!

Related Posts/More Information


Monday, 13 October 2025

Structured Output from Flow Agent Actions in Winter '26

Image created by ChatGPT 5o based on a prompt by Bob Buzzard


Introduction

When preparing for the Credera Winter '26 release webinar, the release notes for this feature gave me significant pause for thought. Not because it was an awesome change that I'd been waiting ages for, nor that it was something out of left field that I couldn't wait to try. Instead it was because I didn't understand how it worked. The release notes talked about custom agent actions returning specific fields, so did that mean it was the action itself that returned complex data types? There was only one way to find out.

Giving it a Go

Once I'd waited for my Agentforce developer edition to be upgraded to Winter '26 I was able to try out this new functionality. In order to understand how much effort I had to put in around my actions, I started off putting in zero effort. Masterful inactivity has always served me well!

The first thing I tried was a simple screen flow, with the first element an AI Agent Action, as this was the key to defining structured output. 


First crack out of the box and I have a winner! Without even having to create a custom action, Copilot for Salesforce is available as an AI Agent Action. Clicking into this showed that the new Structured Output functionality was available with this action.


After a few false starts (the AI Agent Action wouldn't accept collections of records etc) I had a simple flow that would take in an account Id, retrieve the opportunities associated with the account, convert them to a simple JSON structure and ask the AI Agent to calculate the total amount of the opportunities. For my AI Agent Action, I give a relatively simple prompt grounded with the opportunity information.

and for my structured output, I specified a single field - the total amount.


Executing this in debug mode gave me the answer to my first question - did that mean it was the action itself that returned complex data types? No, in this case I'm simply sending a request to an LLM and it will give a text response. The Salesforce platform handles the conversion to structured output.



And I can then use that structured output like I would any other complex object, in this case in a screen displaying the total.


Note that the "container" for the structured output is actually a Dynamic Apex Class that you can access through setup:


Conclusion


To answer my earlier question:

Did that mean it was the action itself that returned complex data types? 

It did indeed - there was no need for me to create anything outside of the action for the results to be stored in, I just defined the field and used natural language to explain what should be stored in there. The platform created an Apex class to store the information and populated it from the LLM output.

This is pretty cool - it allows low code to convert the unstructured output from an LLM into structured output for use in downstream processing. Prior to this feature an Apex developer would likely have been needed to help, but now it can all be handled by a low coder.

Of course this is a terrible example. If I really wanted the total, why wouldn't I calculate it while iterating the records, rather than pulling together a bunch of text and then incurring the expense and time overhead of an LLM callout - Agents Augment Automation, they don't replace it. 

I chose this example as it was easy to put together to prove the concept. In the real world I'd only use LLMs to handle tasks that regular automation couldn't, like figuring out the customer sentiment from a bunch of activities associated with the Opportunities. More work to set that up though, and harder to explain.

More Information



Tuesday, 7 October 2025

Agentforce Vibes - First Look, Data Model

  Image created by ChatGPT 5 based on a prompt from Bob Buzzard 


Introduction

We all knew this was coming, right? Salesforce has long considered itself the cool kid in enterprise technology, so they were always going to jump on the vibe coding bandwagon. After reading the Salesforce Developers blog post on Agentforce Vibes I was keen to give it a go. 

I took the approach that I wanted the Agent to be truly autonomous, so my plan was to agree with everything it wanted to do, and then once everything was deployed to my org, I'd try it out and review everything at that point. This is how I'd work with a human junior assisting me, although I'd obviously be available to talk through their ideas if they wanted, which agents typically don't need.

Setup

Setup was as easy as it gets. I'm using VS Code and simply by switching to the Agent Dev view we were off and vibing.


I spun up a scratch org, activated the MCP server for the Salesforce CLI, and then tried to figure out what to do with it.

What to Vibe Out

I didn't just want to vibe some straightforward additional Apex to an existing code repository, as I'm sure that's one of the smoke tests of this new functionality. If it can't do that, it's going to be a tough time on the socials for Salesforce. The part of application development I've always wanted to speed up, especially for my side projects, is creating the data model and permission sets. Doing this directly through XML metadata is quite error-prone, and doing it through Setup takes a while. As a coder, this is typically a task I just want out of the way so I can start cutting some Apex. 

I decided to give it the kind of task that I was intimately familiar with, as I'd given it to many graduates back in my BrightGen days. The concept is an onboarding application, with a bunch of templated journeys broken down into steps that can be instantiated and assigned to a new joiner, with a specified start date and manager etc. There's a bunch of requirements around calculating completion dates and current state that require roll up summaries and formula fields, so it's a good introduction to data modelling for those new to Salesforce.

I created a prompt of around 130 words that covered the key concepts in natural language. I avoided giving any clues, so rather than talking about roll up summaries and master details, I used phrases like "this is calculated from the max values in the steps for the journey". Probably quite close to the real instructions that I gave humans.

I gave the agent the prompt and asked it to generate a plan, which it did. 

The plan was frankly excellent.

It had picked up all the nuance of the requirements - identified where Master-Detail relationships were required, understood that templates were separate to the actual journeys and needed to be modelled as their own objects, and came up with recommendations around security and deployment. It also suggested a bunch of extra fields, permission sets, and a flow to create journeys from templates. Most of this was later tasks for the grads so I told it to skip those. I then signed off on the plan and sat back to watch the agent at work.

Creating the Metadata

One thing I found a little tedious was the agent wanted me to okay every file before creating it, even though I'd ticked the box for auto-approve. I didn't check any further into this, so it's possible there's another setting I needed to look at. I typically don't review people's work piecemeal as they create individual components, so I just okayed them all straight away. What I saw as they were being generated looked plausible, and after about 10 minutes it had completed all the work. So I asked to to deploy its work to my scratch org.

Deploying the Files (or where it all went awry)

The initial attempt at deployment threw up an error that you can't specify both the apexTests and apexTestLevel parameters. Slightly unexpected, but it was easily able to move on. 

The next attempt threw up a few errors:

  • The agent had used <picklist> instead of <valueSet> which wasn't compatible with the metadata API version I'd specified. Slightly unexpected again, as I'd been asked which API version I wanted to generate the metadata for, but again something easily fixed.
  • It had set a Private sharing model for an object on the detail side of a Master-Detail relationship. It turns out this was for the sharing model and the external sharing model, which caused problems in later attempts as it changed one but not the other.
  • The roll up summary metadata fields weren't correct, so the agent suggested changing them. It turned out that the suggested new fields were no more valid (<summaryTable>).
As the agent was in charge, I okayed all of its suggested changes and it tried again. We then entered a doom cycle of attempting to deploy, getting errors, and highly variable suggestions for fixes. 

I think that the agent wanted to apply fixes for every error in a one go, which isn't always the best approach with deployments, as one error can cascade into a lot of failures. My approach is to fix errors one at a time and retry the deployment, so that I have a handle on what I've changed and what difference it made. The agent would want to change the metadata to fix every error at once, even when the underlying error was a custom object failing to deploy. 

My favourite was where a parent object in a relationship couldn't deploy because the roll up summary metadata was wrong, which threw an error on the child object. The agent felt that this was an issue with the child object and the case was the relationship being Master-Detail. It changed the relationship  to a Lookup field, but sadly it left the roll up summary metadata in place, thus finding more errors at the next deployment.

After a couple of attempts the agent had used up all my requests and switched me to the core model. I wondered if this might be better, given that it's a Salesforce hosted (and presumably trained) model, but sadly that wasn't the case. If I was paying for requests to be burned by something that wasn't even following documented metadata standards, I'd likely be a little miffed.

Eventually the agent proudly announced that it had completed the deployment, even though I could see the request had failed.



Creating and Deploying Permission Sets (or the Folie à Deux


Checking the org also confirmed nothing had been deployed, but this is vibe coding where the facts don't matter and the agents are in charge, so I feigned ignorance and asked it to now create some permission sets for me - an admin and a manager, obviously giving quite a lot of detail.

Again, the plan here was excellent - it understood my prompt, picked out the nuance and generated plausible files. The agent had clearly been emboldened by how easily I was tricked into believing the deployment was successful, and jumped straight to it. This time I decided I couldn't continue to enable its flights of fancy and called it out. It folded like a cheap suit. 



This was comfortingly familiar - often ChatGPT and others give me completely incorrect code and when called out on it, fess up immediately. It didn't offer to fix it again though, just told me it was sorry, the system was broken, and what needs fixing. Vibe Confessions.

At this point I took over and fixed the errors - <writable> instead of <editable> for a custom field in the permission set was the most egregious, in case you were wondering. 

But Seriously Folks!


It's easy to mock Agentforce Vibes (I mean, just look at what it was doing - this stuff writes itself!), but the issues I've identified will be easily fixed. It reminds me of the first release of Agentforce for Developers - the test class code generated by that was fairly awful, but it wasn't long before it was quite reliable. If we didn't have Dreamforce '25 starting in a week, I'm pretty sure we wouldn't be seeing this yet. I guess that data model metadata might also not be its strong suit, but it's GA and it understood the ask, so I think it's fair enough to call out the performance.

So for this, admittedly slightly complex, data modelling task, Agentforce Vibes is top tier for planning, but decidedly middling for execution. You'll need to be experienced with Salesforce metadata to guide it through generating the correct metadata, or fixing it yourself. In terms of generating a list of tasks to carry out in the UI, it was pretty amazing, it just wasn't great at handling those tasks itself in metadata. 

While the above probably reads as somewhat negative (and yes, snarky!), that isn't really the case. Using Agentforce Vibes was way faster than trying to create this all myself.  Either via XML or through the UI. I probably had it all in my scratch org with appropriate permission sets in around 60-90 minutes. The caveat here is that if I didn't have my many years of experience working with metadata, I doubt I'd have got it deployed at all. 

Once the execution catches up with the planning, which I'm sure won't be that long, it will be a different story and a really helpful sidekick.

This is only my first look - I'll be back to this scenario to vibe code some flow, lightning component and asynchronous Apex and I'll keep you updated on how I got on.

Related Information