LWC State Management and Apex Cursors
Build a promotion wizard with shared LWC state and Apex cursor pagination.
Introduction
30 Second Summary
Think about how multi-step online forms remember everything you typed on page one when you reach page three. Behind the scenes, something manages that shared state across screens.
In this project, you will complete a multi-step Promotion Creation Wizard in Salesforce by implementing shared state management across Lightning Web Components, cursor-based pagination for large datasets, and enabling an AI-powered Employee Agent.
What You'll Build
A multi-step Promotion Creation Wizard powered by LWC State Management (Beta) and Apex Cursors (Beta), deployed on an Account record page in a Consumer Goods Cloud trial org.
By the end of this project, you'll have:
- A shared state manager that synchronizes data between parent and child LWC components without prop drilling.
- A cursor-based pagination system in Apex that efficiently fetches Product2 records in pages.
- A deployed promotion wizard with a custom Account action button that creates Promotion records with linked products and stores.
- An enabled Agentforce Employee Agent ready for field rep queries.
- Secret Mission: Configure the Employee Agent with a custom topic and action to validate promotion product prices at retail stores.
Are there any prerequisites?
Yes. You need VS Code with the Salesforce Extension Pack installed, Salesforce CLI (sf) installed, and a Consumer Goods Cloud trial org authorized.
You also need the af360league-apexandlwc repo cloned and the custom objects and metadata already deployed to your org.
Set Up and Verify Your Environment
Before you write any code, you need to confirm that your development environment is ready to go.
This project uses two Beta Salesforce features that require a properly authorized org with pre-deployed metadata. Let's verify everything is in place, then explore the component architecture you'll be building on.
In this step, get ready to:
- Verify your org authorization and deployed metadata.
- Confirm field permissions on the Promotion Product object.
- Map the component architecture and identify incomplete files.
Verify your org authorization and deployment
Your Consumer Goods Cloud trial org should already be authorized from the prerequisites. Let's confirm the connection is still active.
- In VS Code, open the integrated terminal by pressing Ctrl+` (macOS and Windows).
- Confirm your org is authorized by running this command:
sf org display
You should see a table with your org's Connected Status, Instance URL, and Username.
✔️ I see my org details
Your org is authorized and ready. Move on to the next substep.
ⓧ I see an error or no org
- Re-authorize your org by running:
sf org login web
This opens a browser window where you can log into your Consumer Goods Cloud trial org. After logging in, return to VS Code and run sf org display again to confirm.
If your objects and custom metadata are not yet deployed, run:
sf project deploy start --source-dir force-app/main/default/objects,force-app/main/default/customMetadata
Wait for the deployment to finish successfully before continuing.
Verify field permissions and page layout
The Promotion Product object needs two custom fields accessible to your profile. You configured these in the prerequisites, so let's quickly confirm they're in place.
- In your browser, open your Salesforce org's Setup page (click the gear icon in the top-right corner, then Setup).
- In the Quick Find box, type Object Manager and click Object Manager.
- Search for and select Promotion Product.
- Click Fields & Relationships in the left sidebar.
- Confirm you can see both Target Shelf Price and Discount percent in the field list.
Why do these fields matter?
The Discount percent field stores the discount percentage each product receives in a promotion. The Target Shelf Price field is used by the Agentforce Employee Agent in the Secret Mission to validate prices at retail stores.
Understand the component architecture
The promotion wizard is built from five LWC components that share state using the LWC State Manager Beta API.
- In VS Code's Explorer sidebar, expand force-app/main/default/lwc.
You should see folders for each promotion wizard component. Here is how they relate to each other:
- promotionStateManager is a standalone API module with no HTML template. It defines the shared state that all wizard steps read from and write to.
- promotionCreationWizard is the parent component. It creates the state manager instance, making it the provider.
- promotionWizardStep1 is a consumer component for entering the promotion name.
- promotionWizardStep2 is a consumer component for selecting products with cursor-based pagination.
- promotionWizardStep3 is a consumer component for selecting stores. This one is already complete.
What is the provider/consumer pattern?
The parent component (provider) creates the state manager instance. Child components (consumers) use fromContext to retrieve the closest ancestor's state manager without prop drilling.
This means any child component anywhere in the tree can read and update the same shared state. Changes in one step are immediately visible to all other steps.
Now let's identify what's incomplete. Each file below has TODO comments marking the code you'll add in the next steps:
- Open promotionStateManager.js and look for TODO comments. It's missing the defineState import, the atom/computed/setAtom parameters, atom initializations, and computed definitions.
- Open promotionCreationWizard.js. It's missing the state manager import and the promotionState initialization.
- Open promotionWizardStep1.js. It's missing the fromContext import, state manager import, promotionState field, and the updatePromotionName call.
- Open promotionWizardStep2.js. It's missing the fromContext import, state manager import, and promotionState field.
- Open PromotionCreatorCtrl.cls in force-app/main/default/classes. It's missing the Apex Cursor pagination logic including cursor creation, fetch calls, and serialization.
Keep these files open
You'll be editing all five of these files across the next three steps. Keep them open in VS Code tabs so you can switch between them quickly.
Your environment is verified and you know exactly which files need completing. Next up, you'll implement the core state management logic in the promotionStateManager.
Wire State Management into Wizard Components
Your state manager is complete and ready to serve as the single source of truth for the promotion wizard.
Now you need to connect the parent and child components to it. The parent component creates the state manager instance (making it the provider), and child components use fromContext to retrieve it (making them consumers).
In this step, get ready to:
- Wire the parent wizard component as the state provider.
- Connect Step 1 and Step 2 child components as state consumers.
- Save user input to shared state from the child components.
Complete promotionCreationWizard.js (provider)
The parent wizard component is the one that creates the state manager instance. Because it sits at the top of the component tree, all child components nested inside its template can access this same instance.
- Open force-app/main/default/lwc/promotionCreationWizard/promotionCreationWizard.js in VS Code.
- Find the existing import lines at the top of the file.
- Add the state manager import after the other imports by pasting this line:
import promotionStateManager from 'c/promotionStateManager';
- Inside the class body, find the TODO comment for the state manager initialization.
- Replace that TODO comment with this class field:
promotionState = promotionStateManager();
What does this code do?
Calling promotionStateManager() creates a new instance of the state manager you built in Step 2.
Because this parent component creates the instance, it becomes the provider. Any child component in the template can now consume this exact instance using fromContext.
- Save promotionCreationWizard.js.
You won't see a change yet. The provider needs consumers connected before the state flows anywhere.
Complete promotionWizardStep1.js (consumer)
Step 1 of the wizard handles promotion name entry. It needs to read from and write to the shared state so the name persists when navigating between steps.
- Open force-app/main/default/lwc/promotionWizardStep1/promotionWizardStep1.js.
- Add the imports for fromContext and the state manager at the top of the file, below the existing lwc import:
import { fromContext } from '@lwc/state';
import promotionStateManager from 'c/promotionStateManager';
- Inside the class body, find the TODO comment and replace it with the fromContext binding:
promotionState = fromContext(promotionStateManager);
What is fromContext?
fromContext tells the LWC State Manager to look up the component tree for the nearest ancestor that created an instance of the given state manager.
It finds the promotionState = promotionStateManager() you just set in the parent wizard, and connects this child to the same reactive state.
- Now find the allValid() function in the same file.
- Locate the line return true; inside that function.
- Add this line directly before return true; to save the promotion name into shared state:
this.promotionState.value.updatePromotionName(this.promotionName);
What does this line do?
When the user clicks Next on Step 1, allValid() fires. This line calls the updatePromotionName action you defined in the state manager, persisting the name into the shared promotionName atom.
The .value property is how consumer components access the public API of the state manager.
- Save promotionWizardStep1.js.
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
Here is the full promotionWizardStep1.js file for comparison:
import { LightningElement, api } from 'lwc';
import { fromContext } from '@lwc/state';
import promotionStateManager from 'c/promotionStateManager';
export default class PromotionWizardStep1 extends LightningElement {
promotionState = fromContext(promotionStateManager);
promotionName;
connectedCallback(){
this.promotionName = this.promotionState?.value?.promotionName;
}
handleChange(event) {
this.promotionName = event.detail.value;
}
@api
allValid(){
if(this.promotionName === undefined || this.promotionName === ''){
return false;
}
this.promotionState.value.updatePromotionName(this.promotionName);
return true;
}
}
Complete promotionWizardStep2.js (consumer)
Step 2 handles product selection with pagination. It already contains code that references this.promotionState.value.chosenProducts and this.promotionState.value.updateProducts(). You just need to wire up the connection.
- Open force-app/main/default/lwc/promotionWizardStep2/promotionWizardStep2.js.
- Add the imports for fromContext and the state manager at the top of the file, below the existing lwc import:
import { fromContext } from '@lwc/state';
import promotionStateManager from 'c/promotionStateManager';
- Inside the class body, find the TODO comment and replace it with the fromContext binding:
promotionState = fromContext(promotionStateManager);
Why is Step 2 simpler to wire?
Step 2 already has code that reads from and writes to the state manager. All it was missing was the fromContext binding that connects it to the provider instance.
This is the power of the provider/consumer pattern. Components don't need to pass props down the tree. They just declare what state manager they need, and the framework wires it up.
- Save promotionWizardStep2.js.
- Confirm that VS Code shows no red underlines or syntax errors in any of the four files: promotionStateManager.js, promotionCreationWizard.js, promotionWizardStep1.js, and promotionWizardStep2.js.
Seeing red underlines in VS Code?
- Check that the import paths match exactly. The state manager import must be 'c/promotionStateManager' (lowercase c, which is the LWC namespace prefix for custom components).
- Ensure the fromContext import comes from '@lwc/state', not from 'lwc'.
- Verify the class field uses fromContext(promotionStateManager) without parentheses after promotionStateManager. Consumers pass the definition, not an instance.
Help me debug import errors in my LWC state manager consumer components.
All your components are now wired together through a shared reactive state layer. Next up, you will implement the Apex cursor logic that powers the product list pagination in Step 2.
Implement Apex Cursor Pagination
Your LWC components can now share state seamlessly. The next challenge is the product list in Step 2 of the wizard.
Loading every Product2 record at once would be slow and wasteful. Apex Cursors solve this by holding a reference to the full result set server-side, letting you fetch pages on demand without re-executing the SOQL query each time.
In this step, get ready to:
- Create or deserialize a Database.Cursor for SOQL pagination.
- Calculate the fetch window and retrieve records from the cursor.
- Serialize the cursor back to the LWC for reuse across page requests.
Create or deserialize the cursor
The getProducts method needs to handle two scenarios. On the first call, it creates a fresh cursor. On subsequent page requests, it rebuilds the cursor from a serialized string the LWC sends back.
- Open force-app/main/default/classes/PromotionCreatorCtrl.cls in VS Code.
- Find the cursor TODO block inside the getProducts method (below the Database.Cursor locator; declaration).
- Replace the TODO comment with the following cursor creation logic:
if (locatorParam != null && locatorParam != '') {
locator = (Database.Cursor) JSON.deserialize(locatorParam, Database.Cursor.class);
} else {
locator = Database.getCursor('SELECT Id, Name, cgcloud__Category__c FROM Product2');
}
What does this code do?
This if/else handles two distinct states of the pagination flow:
- When locatorParam is not empty, the LWC has sent back a previously serialized cursor. JSON.deserialize reconstructs the Database.Cursor object from that string.
- When locatorParam is null or empty (first page load), Database.getCursor executes the SOQL query and returns a Cursor holding a reference to the full result set without loading all records into memory.
You won't see a change yet. The next additions fetch the actual records from this cursor.
Calculate the fetch window and retrieve records
The cursor now exists, but we need to tell it which slice of records to return. The countToFetch calculation ensures we never request more records than remain in the result set.
- Find the line Integer position = (pageNumber - 1) * pageSize; in the getProducts method.
- Add this line directly below it to calculate how many records to fetch:
// Fetch the lesser of page size or remaining records to avoid over-fetching
Integer countToFetch = Math.min(pageSize, locator.getNumRecords() - position);
- Next, find the result.pageNumber = pageNumber; line further down.
- Add the following two lines below it to set the total count and fetch the records:
// Total records lets the LWC calculate how many pages exist
result.totalItemCount = locator.getNumRecords();
// Fetch the current page of records from the cursor at the calculated offset
result.records = locator.fetch(position, countToFetch);
What does this code do?
- Math.min(pageSize, locator.getNumRecords() - position) prevents fetching beyond the end of the result set. If only 3 records remain but the page size is 5, it fetches 3.
- locator.getNumRecords() returns the total row count across the entire cursor. The LWC uses this to calculate total pages for its pagination UI.
- locator.fetch(position, countToFetch) retrieves records starting at the offset position. Each call to fetch can return up to 2000 records maximum.
You won't see a change yet. The final piece serializes the cursor so it can travel back to the LWC.
Serialize the cursor for reuse
AuraEnabled methods are stateless. The cursor object only lives for the duration of one server call. To reuse it on the next page request, you serialize it into a JSON string and send it back to the LWC.
- Find the result.records line you just added.
- Add this line below it to serialize the cursor for the LWC:
// Send the cursor back as a string so the LWC can pass it on the next page request
result.locator = JSON.serialize(locator);
Why serialize the cursor?
JSON.serialize(locator) converts the Database.Cursor into a string the LWC stores in its locator property. On the next page request, that same string comes back as locatorParam and gets deserialized in the if-block you wrote in Substep 1.
This round-trip pattern avoids re-executing the SOQL query for every page. The cursor holds a stable reference to the result set.
- Save PromotionCreatorCtrl.cls.
- Check the Problems panel in VS Code (press Cmd+Shift+M on macOS or Ctrl+Shift+M on Windows). You should see no Apex errors for this file.
Seeing syntax errors?
- Make sure the if/else block sits between the Database.Cursor locator; declaration and the Integer position line. The order matters because position uses locator later.
- Check that countToFetch is declared before result.records = locator.fetch(position, countToFetch); otherwise Apex cannot resolve the variable.
- Confirm you typed Database.Cursor.class (capital C) in the deserialize call. Apex is case-sensitive for class references.
Help me debug an Apex Cursor compilation error in PromotionCreatorCtrl
✔️ Awesome, I've got everything!
Great. Double check you have saved PromotionCreatorCtrl.cls before moving on.
ⓧ I'd like to double check the full code
Here is the complete PromotionCreatorCtrl.cls file. Compare your code against this:
public with sharing class PromotionCreatorCtrl {
@AuraEnabled
public static PagedResult getProducts(
String type,
Integer pageNumber,
String locatorParam
) {
Integer pageSize = 5;
Database.Cursor locator;
if (locatorParam != null && locatorParam != '') {
locator = (Database.Cursor) JSON.deserialize(locatorParam, Database.Cursor.class);
} else {
locator = Database.getCursor('SELECT Id, Name, cgcloud__Category__c FROM Product2');
}
Integer position = (pageNumber - 1) * pageSize;
Integer countToFetch = Math.min(pageSize, locator.getNumRecords() - position);
PagedResult result = new PagedResult();
result.pageSize = pageSize;
result.pageNumber = pageNumber;
result.totalItemCount = locator.getNumRecords();
result.records = locator.fetch(position, countToFetch);
result.locator = JSON.serialize(locator);
return result;
}
@AuraEnabled
public static List<RetailStore> getRetailStores(String accountId) {
return [
SELECT Id, Name, RetailLocationGroup.Name
FROM RetailStore
WHERE AccountId = :accountId
ORDER BY Name
];
}
@AuraEnabled
public static SaveResult savePromotion(String promotionDataJson) {
SaveResult result = new SaveResult();
Savepoint sp = Database.setSavepoint();
try {
PromotionData data = (PromotionData) JSON.deserialize(
promotionDataJson,
PromotionData.class
);
if (String.isBlank(data.promotionName)) {
throw new AuraHandledException('Promotion name is required.');
}
if (data.products == null || data.products.isEmpty()) {
throw new AuraHandledException(
'At least one product must be selected.'
);
}
if (data.stores == null || data.stores.isEmpty()) {
throw new AuraHandledException('At least one store must be selected.');
}
Promotion promotion = new Promotion();
promotion.Name = data.promotionName;
promotion.StartDate = Date.today();
promotion.EndDate = Date.today().addMonths(1);
insert promotion;
List<PromotionProduct> promotionProducts = new List<PromotionProduct>();
for (ProductData prod : data.products) {
PromotionProduct pp = new PromotionProduct();
pp.PromotionId = promotion.Id;
pp.ProductId = prod.productId;
pp.Discount_percent__c = prod.discountPercent;
promotionProducts.add(pp);
}
if (!promotionProducts.isEmpty()) {
insert promotionProducts;
}
List<PromotionChannel> promotionChannels = new List<PromotionChannel>();
for (StoreData store : data.stores) {
PromotionChannel pc = new PromotionChannel();
pc.PromotionId = promotion.Id;
pc.RetailStoreId = store.storeId;
pc.StartDate = Date.today();
pc.AccountId = data.accountId;
promotionChannels.add(pc);
}
if (!promotionChannels.isEmpty()) {
insert promotionChannels;
}
result.success = true;
result.promotionId = promotion.Id;
result.promotionName = promotion.Name;
result.message =
'Promotion created successfully with ' +
promotionProducts.size() +
' product(s) and ' +
promotionChannels.size() +
' store(s).';
} catch (DmlException e) {
Database.rollback(sp);
result.success = false;
result.message = 'Database error: ' + e.getDmlMessage(0);
throw new AuraHandledException(result.message);
} catch (Exception e) {
Database.rollback(sp);
result.success = false;
result.message = 'Error creating promotion: ' + e.getMessage();
throw new AuraHandledException(result.message);
}
return result;
}
public class PagedResult {
@AuraEnabled
public Integer pageSize;
@AuraEnabled
public Integer pageNumber;
@AuraEnabled
public Integer totalItemCount;
@AuraEnabled
public List<SObject> records;
@AuraEnabled
public String locator;
}
public class PromotionData {
@AuraEnabled
public String promotionName;
@AuraEnabled
public String accountId;
@AuraEnabled
public String templateId;
@AuraEnabled
public Date startDate;
@AuraEnabled
public Date endDate;
@AuraEnabled
public List<ProductData> products;
@AuraEnabled
public List<StoreData> stores;
}
public class ProductData {
@AuraEnabled
public String productId;
@AuraEnabled
public String productName;
@AuraEnabled
public String category;
@AuraEnabled
public Decimal discountPercent;
}
public class StoreData {
@AuraEnabled
public String storeId;
@AuraEnabled
public String storeName;
@AuraEnabled
public String locationGroup;
}
public class SaveResult {
@AuraEnabled
public Boolean success;
@AuraEnabled
public String promotionId;
@AuraEnabled
public String promotionName;
@AuraEnabled
public String message;
}
}
How does the full pagination flow work?
The round-trip between the LWC and Apex follows this cycle:
- The LWC calls getProducts with a pageNumber and locatorParam (null on the first call).
- Apex creates or deserializes the cursor, fetches the correct slice of records, and returns them along with the serialized cursor.
- The LWC stores the returned locator string and sends it back on the next page request, so the same cursor is reused.
Your Apex controller can now serve paginated product data efficiently. Next up, you will deploy all the code and test the full promotion wizard end-to-end.
Deploy and Test the Promotion Wizard
You have completed all the code across four LWC components and one Apex controller. The state manager defines shared atoms, the parent wizard provides the state instance, and the child steps consume it via fromContext.
Now it is time to deploy everything to your org and wire up the wizard so users can launch it from an Account record page. You will create a Quick Action button that opens the wizard, then test the full promotion creation flow end-to-end.
In this step, get ready to:
- Deploy all LWC and Apex code to your org.
- Create an Account Quick Action that launches the promotion wizard.
- Test the wizard by creating a Promotion with linked products and stores.
Deploy all code to the org
Your components are complete locally, but Salesforce cannot use them until they exist in the org. Deploying pushes all LWC and Apex source from your local force-app directory into your connected org.
- Deploy the entire project by running this command in your VS Code terminal:
sf project deploy start --source-dir force-app
What does this command do?
The sf project deploy start command pushes your local source files to the authorized org.
The --source-dir force-app flag scopes the deployment to only the force-app directory, which contains all your LWC components, Apex classes, objects, and custom metadata.
- Wait for the deployment to complete. You should see a success message listing all deployed components.
Seeing deployment errors?
If you see errors related to the LWC State Manager or Apex Cursors features not being recognized, confirm that your org has these Beta features activated. Check the sourceApiVersion in your sfdx-project.json file matches your org's API version (61.0 or higher).
If you see a syntax error, double check your completed files against the full code in the next tab group below.
Help me debug my deployment error.
Create the Account Quick Action
A Quick Action is a button that appears on a record page and triggers a specific action. You will create one that launches the promotionCreationWizard LWC directly from any Account record.
- In your Salesforce org, click the gear icon in the top-right corner and select Setup.
- In the Quick Find search box on the left, type Object Manager and click Object Manager.
- Find and click Account in the object list.
- Click Buttons, Links, and Actions in the left sidebar.
- Click New Action in the top right.
Fill in the action form with these values:
- Set Action Type to Lightning Web Component.
- Set Lightning Web Component to c:promotionCreationWizard.
- Set Label to Create Promotion.
- The Name field should auto-populate to Create_Promotion.
- Click Save.
Why a Lightning Web Component action type?
Choosing Lightning Web Component as the action type tells Salesforce to render your LWC inside the action modal. The recordId property in your wizard automatically receives the Account's ID, which is how Step 3 knows which stores to display.
Now add the action to the Account page layout so it appears on record pages.
- In the Account object, click Page Layouts in the left sidebar.
- Click your active Account page layout (usually Account Layout).
- In the layout editor, click Salesforce Mobile and Lightning Experience Actions in the top palette.
- If you see an override the predefined actions link in the actions section, click it first.
- Drag Create Promotion from the palette into the Salesforce Mobile and Lightning Experience Actions section.
- Click Save.
Don't see the Create Promotion action in the palette?
Make sure you saved the Quick Action in the previous step. Try refreshing the page layout editor. The action should appear under the Salesforce Mobile and Lightning Experience Actions category in the top palette.
Help me find my Quick Action on the page layout.
Test the wizard end-to-end
With the action deployed and added to the layout, you can now run through the full promotion creation flow.
- Navigate to the Consumer Goods Cloud app using the App Launcher (grid icon in the top-left corner).
- Open any Account record.
- Click the Create Promotion action button in the record page actions bar.
The wizard modal should open. Now test each step:
- Step 1: Enter a promotion name (for example, Summer Water Promo) and click Next.
- Step 2: Select one or more products by checking their checkboxes. Enter a discount percentage for each selected product. Use the pagination buttons to browse additional pages. Click Next.
- Step 3: Select one or more stores, then click Create Promotion.
You should see a success toast message confirming the promotion was created.
- After the toast appears, you will be navigated to the new Promotion record page.
- Scroll down to the Related Lists section. Verify that Promotion Products shows your selected products with their discount percentages.
- Verify that Promotion Channels shows your selected stores.
What just happened?
Your wizard used the LWC State Manager to pass the promotion name, selected products, and selected stores across three separate child components without prop drilling.
The Apex Cursor pagination in Step 2 fetched products in pages of 5 without rerunning the SOQL query each time. The serialized cursor maintained its position across each page request.
Wizard not opening or seeing errors?
If the button does not appear, hard-refresh the page (Cmd+Shift+R on macOS, Ctrl+Shift+R on Windows). Salesforce caches page layouts aggressively.
If the wizard opens but throws errors when paginating or saving, open the browser console (F12) and check for Apex error messages. Common causes are missing field permissions on Discount_percent__c or the cgcloud__Category__c field not being accessible.
Help me debug my promotion wizard errors.
Your promotion wizard is fully deployed and working. Next up, you will take on the Secret Mission and configure an Agentforce Employee Agent for AI-powered price validation.
Secret mission
Enable and Configure the Agentforce Employee Agent
Your promotion wizard is working. But what happens when a field rep visits a store and spots a price that doesn't match the promotion? In this secret mission, you'll enable Agentforce and create an Employee Agent that can validate product prices at retail stores in real time.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely within a Salesforce trial org, so there are no ongoing costs.
Resources you created:
- Test Promotion records (with linked PromotionProduct and PromotionChannel records) in the trial org.
- Agentforce Employee Agent configuration in Agent Studio.
- Account Quick Action (Create Promotion) on the Account page layout.
- Deployed LWC components and Apex class in the trial org.
- Local code edits in the af360league-apexandlwc project folder.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep the wizard deployed for the Agentforce 360 League challenge validation.
- Your trial org remains active for 30 days from creation.
- All deployed components, test data, and the Employee Agent stay available.
- Your local project files in af360league-apexandlwc remain on your machine.
Pause - I'll come back to this later
There is nothing to shut down or pause for this project. Your Salesforce trial org does not incur charges while idle.
- Your trial org will automatically expire 30 days after creation. If you plan to return, do so before the expiry date.
- Your local code edits remain saved in the af360league-apexandlwc folder.
Delete - I don't want to use this again
Remove all project resources and start fresh. Since this is a free trial org, there are no paid resources to worry about.
- Delete test Promotion records: In the Consumer Goods Cloud app, navigate to the Promotion record you created. Click the dropdown arrow next to the record name and select Delete.
- Delete the Employee Agent: In Setup, search for Agents in Quick Find. Click Agents under Agent Studio. Find Employee Agent, click the dropdown arrow, and select Delete.
- Remove the Quick Action from the Account page layout: In Setup, go to Object Manager, then Account, then Page Layouts. Edit the layout, find Create Promotion in the Salesforce Mobile and Lightning Experience Actions section, and remove it.
- Delete the Quick Action: In Object Manager, go to Account, then Buttons, Links, and Actions. Find Create Promotion, click the dropdown arrow, and select Delete.
- Delete local code edits (macOS/Linux):
cd ~/Desktop
rm -rf af360league-apexandlwc
- Delete local code edits (Windows):
cd ~\Desktop
rmdir /s /q af360league-apexandlwc
The trial org itself will automatically expire after 30 days. No manual deletion is required for the org.
Nice Work!
Nice Work!
Nice work! You have just built a full-stack Promotion Creation Wizard using two Beta Salesforce features and connected it to a live Account record page.
You've learned how to:
- Implement LWC State Management (Beta) using defineState, atom, computed, and setAtom to create a shared state manager, and use the fromContext pattern to share state between parent provider and child consumer components without prop drilling.
- Implement Apex Cursors (Beta) for efficient server-side pagination using Database.getCursor and cursor.fetch, serializing and deserializing cursor objects between client-server calls for stateless reuse.
- Deploy and test a full promotion creation wizard end-to-end with a custom Account quick action that creates Promotion, PromotionProduct, and PromotionChannel records.
- Secret Mission: Enabled and configured an Agentforce Employee Agent with a custom price validation topic for AI-powered field rep queries.
Ready to quiz yourself?