Skip to main content
Group

Apex Developer Previews and Betas

For collaboration and discussion of current Apex Developer Preview and Beta features.

I tried it and while I didn't get any errors I could see from the JSON payloads in my LWC component there was no data being exchanged, e.g. the queryId. I suspect this could be a security rational - but figured I would post here to confirm. I see that Apex Session Cache is an alternative in this use case. cc @Dominic Foster

1 answer
  1. Oct 8, 2025, 11:55 PM

    Hi Andy, 

    No this is a bug in the current Beta that we will be addressing as part of going GA. 

     

    As a short term solution you can JSON.serialize and JSON.deserialize for the client round trip to persist the cursor. Or, as you found, use the Platform Cache to keep track of it for the user. 

     

     

0/9000

While the general Salesforce REST API Cursor docs indicate the number of cursors is no longer limited - the Apex Governor Limits docs do state a 10k per day limit. Thus if this remains true for GA it would be good to have a way to write code the manages this finite resource by including deletes when possible. This this is a request for a deleteCursor method. Thanks. cc @Dominic Foster

0/9000
0/9000
0/9000

At TDX, Salesforce announced the launch of a Beta to make Apex REST APIs available as Custom Agent Actions in Agentforce!  You can leverage your existing investments in custom Apex REST APIs with Agents to improve their capabilities and the level of service they offer.

 

With the Beta you can generate an OpenAPI Document describing the Apex REST API, deploy that to the API Catalog, and then create custom agent actions from the operations in the Apex REST API.  Then your agents can use these custom actions to invoke your Apex REST APIs when responding to prompts!

 

This demo shows this in action and the documentation describes the steps you follow to achieve this.

 

We need your feedback on how your Apex REST APIs will power your agent journey with Salesforce.  Please post your questions and/or feedback on the Beta to the

Agentforce for Developers Trailblazer Community.  

0/9000

@Dominic Foster I've found a problem with the FormulaEval feature, and custom lookups. Is this a current limitation?

 

It works fine with standard lookups, but when trying to use custom lookups we get a GACK back.

 

To reproduce...

1. Create a custom lookup field on the Contact object to an Account, e.g. Other_Account__c.

2. Create a contact and populate both the Account (standard lookup) and Other_Account__c (custom lookup) with the same record.

3. Run the following apex code, changing the Id in the query to the contact id you just created.

 

Contact theContact = [SELECT Account.Name, Other_Account__r.Name FROM Contact WHERE Id ='003JX00000QMo5dYAD'];

 

FormulaEval.FormulaInstance formulaInstance = Formula.builder()

.withType(Contact.SObjectType)

.withFormula('Account.Name')

.withReturnType(FormulaEval.FormulaReturnType.STRING)

.build();

 

System.debug(formulaInstance.evaluate(theContact));

--- THIS WILL WORK FINE AND THE DEBUG WILL SHOW THE ACCOUNT NAME.

 

--- NOW CHANGE THE FORMULA TO Other_Account__r.Name FROM Account.Name

FormulaEval.FormulaInstance formulaInstance = Formula.builder()

.withType(Contact.SObjectType)

.withFormula('Other_Account__r.Name')

.withReturnType(FormulaEval.FormulaReturnType.STRING)

.build();

 

System.debug(formulaInstance.evaluate(theContact));

 

--- YOU NOW GET AN EXCEPTION IN THE LOGS

Error on line 23, column 1: System.UnexpectedException: Salesforce System Error: 250844729-248762 (162743969) (162743969)

(System Code)

9 answers
  1. Feb 12, 2025, 5:39 PM

    @Daniel Best

    Yes, those always worked, but you have to add them to the code... 

    https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_enum_formulaeval_FormulaGlobal.htm

     

     

    e.g.

    FormulaEval.FormulaInstance ff = Formula.builder()

    .withType(Schema.Account.class)

    .withReturnType(FormulaEval.FormulaReturnType.STRING)

    .withGlobalVariables(new List<FormulaEval.FormulaGlobal>{FormulaEval.FormulaGlobal.USER})

    .withFormula('$User.Id')

    .build();

    system.debug(ff.evaluate(new Account()));

0/9000

Hi everyone, 

 

Today I've taken a look and tested the Apex Cursor's Beta on a Developer Pro Sandbox (Unlimited Edition), using a Cursor on a query that returned ~345,000 records, up to a point where I'm getting the following exception message: 

Cursor Rate Limiting Exception : DailyApexCursorRowsLimit Limit exceeded.

Question is, according to the Apex Governor Limits documentation, I have a 100 million limit on cursor daily rows I can fetch, can't tell for sure but I feel like it was less for me, 100M/345k = ~288 tests 

 

Is there a way to check for this limit? I tried using the limits API endpoint (/services/data/v63.0/limits) but it's not returned, also there is no method for this in the Limits class. 

 

Second question is, (can't tell right now, will check tomorrow), but if I remember correctly, is the  Limits.getFetchCallsOnApexCursor() working as expected? the few times I checked in the debug logs it was returning 0, even after fetching 10 times on the cursor for that transaction. 

 

And lastly, reading the Apex Cursors (Beta) documentation, says that Apex cursors have the same expiration limits as API Query cursors, that is, results are available for 2 days. Say you have a LWC that calls an Apex method in the connectedCallback(). This method creates a cursor for a query and returns a set of records (cursor.fetch(0, 200); for example). 

 

Since the query results are stored for 2 days, is it really mandatory to create the cursor each time the components is loaded? How would I access records from a non-expired cursor, if possible? 

 

Thanks for reading, hope my questions make sense :) 

CC: @Dominic Foster

0/9000

@Dominic Foster, some questions regarding Database.Cursor:

 

Type safety

 

Why is Database.Cursor not templated? According to the material I've seen, I can declare and use a cursor like:

Database.Cursor contactCursor = Database.getCursor('SELECT Id FROM Contact');List<Contact> contacts = contactCursor.fetch(0, 200);

I would have expected to have needed to declare the cursor like:

Database.Cursor<Contact> contactCursor = ...;

If we don't have templating, what should happen if I do:

Database.Cursor contactCursor = Database.getCursor('SELECT Id FROM Contact');List<Account> accounts = contactCursor.fetch(0, 200);

Re-used cursors, user mode and sharing

 

Cursors can be passed around through Apex code, and are serializable to allow them to be retained between transactions.

 

  1. If I get a cursor for a user mode query, what happens if that cursor is re-used in a transaction for a different user with different permissions?
  2. If I get a cursor in a without sharing class then perform the fetch in a with sharing class, is sharing applied against the results?

Happy path cursor expiration

Cursors live for up to 48 hours from what I understand. If I'm caching the cursor, given the cursor's current beta API, I understand I have to catch exceptions against the cursor method(s) to detect that it has expired.

 

Handling exceptions in a "happy path" like this, where essentially I just want to know the cursor is expired, is something of an anti-pattern (I've mentioned this with other APIs added "recently" too). Can we please have a simple method that tells me that the cursor is valid for the duration of my transaction (and make sure that if the cursor could expire during the transaction it is either treated as already expired or that it is kept alive to the end of the transaction)?

 

Fetching the final partial scope/chunk

 

From the AMA session it seems that if you ask for too many records from a given offset, an exception is thrown.

 

Could we have a variant of fetch that doesn't throw an exception and instead simply returns as many records as are available?

 

Fetching when records have been deleted

 

According to the AMA session, if records have been deleted, the number of records returned for a given "chunk" can be fewer than requested (or even 0) simply because one or more of the records that would have been returned has been deleted.

 

Could we have a variant of fetch that tries to return the number of records requested, even if records had been deleted since the cursor was obtained? This actually aligns with the previous point where fetch will finally just return as many records as it can, up to the scope size, regardless of how many records have been deleted and whether the cursor's underlying results have been exhausted.

 

This suggests, too, that the cursor should be able to return the "next record offset" through another method that can be called after a fetch. A value of -1 can indicate that there are no further records. A non-negative value could still result in 0 records being returned if all the remaining records have been deleted, but that's OK.

4 answers
  1. Sep 4, 2024, 8:29 AM

    @Dominic Foster thanks for the feedback.

     

    On some of the comments and questions you posed:

     

    >> Happy path cursor expiration

    > Ideally I'd like to be able to guarantee that the cursor would be valid for an entire transaction and that we could do a single upfront check for that. In reality I couldn't do that.

     

    I would approach this by determining the maximum duration for a transaction in the context of execution, double it for good measure, then if the cursor is within this period of expiration treating it as expired.

     

    >> Could we have a variant of fetch that doesn't throw an exception and instead simply returns as many records as are available?

    > Can you please give me an example of how you would want to use that?

    >> Could we have a variant of fetch that tries to return the number of records requested, even if records had been deleted since the cursor was obtained? 

    > Trying to keep track of those gaps over time if we are constantly reindexing would be challenging.

     

    These are both related to how I would want to process data without having potentially wasted iterations outside the API.

     

    It seems to me (in an implementation ignorant way) that the record set is determined on cursor creation as an ordered set of record IDs. If that's the case, keeping track of pagination is as simple as knowing the last processed/returned ID.

    Like any iterator, I want my iteration logic to simply ask for the next value; it happens that the "next value" is a chunk of records due to the way you have organized the API. I would expect that chunk of records to be the same size each time, except when I get the final chunk where I will get between 0 and chunk size records back.

     

    The skipping of now deleted records is something the cursor infrastructure should deal with rather than expecting every implementation to try to deal with this outside the API.

     

    I realize this isn't a good usage example, given the limits on cursor usage, but imagine using a cursor for UI pagination. If I cannot guarantee that the "next value" has the right number of (or even any) records in it, I have to jump serious hoops to try to fetch enough records to satisfy the next "page", while then holding the detail of the possible "overspill" for the subsequent pagination step. Clearly this issue is less visible when applying an "infinite scroll" approach to data loading.

0/9000

At first, I was really excited about this feature because I often use formulas in my developments, such as in triggers to access parent records values. This way, I could avoid an extra query, for example, accessing the Account.Name in Case before insert. But now, trying out this new feature, I'm a bit disappointed because I'm getting null for this specific case.

 

trigger CaseTrigger on Case (before insert) {

    FormulaEval.FormulaInstance nameFormula = Formula.builder().withReturnType(FormulaEval.FormulareturnType.STRING).withType(Case.SObjectType).withFormula('Account.Name').build();

   

    for (Case cs : Trigger.New) {       

System.debug('Apex Formula: ' + nameFormula.evaluate(cs));

        System.debug('Formula: '+cs.Account_Name_Formula__c);

    }

}

Evaluate Dynamic Formulas in Apex

Is it on the roadmap to cover this?

3 answers
  1. Jun 20, 2024, 6:36 PM

    Apex triggers don't bring back all the related records implicitly be design. If you were to debug out the Case objects in the Trigger.New list they would have the AccountID populated, but the Account reference would be null. 

     

    As mentioned, you would need a single bulkified SOQL query to pull in the details for the related Accounts. 

     

    Formulas in Apex will not full that data in for you. By design it only operates on data that is already in memory. 

0/9000

I decided to spend some time with the new FormulaEval beta in Summer '24 and while it seems mostly solid, I think it would add a lot of value if it had options you could not normally support in Formula fields.

 

For example, providing support for maps, or primitives like the Object Class would allow much more powerful options, because it would let you aggregate data into a single object under whatever property name, and then build formulas off of that.

1 comment
  1. Jun 20, 2024, 5:46 PM

    Hi Cody,

     

    Thanks for the feedback. 

     

    With the Map support, do you see the formula instance being provided a Map as input and then pulling any values out based on the Map keys?

     

    This would present some challenges with the current builder approach where we perform validation that the related object has the required fields/properties for the formula to reference. Referencing a map would need to be more dynamic with runtime validation. 

     

    Can you provide an example where you would use this functionality. Having a use case would be helpful in validating possible solutions we could come up with.

0/9000