1. Overview
The QuickBooks Budget Sync brings QuickBooks Online (QBO) Budgets into Salesforce so budget figures can be reported on alongside CRM and actuals data. Each QBO Budget is stored as a cm_finance__Quickbooks_Budget__c record, and every budget line (one amount per account/customer/class/department per period) is stored as a child cm_finance__Quickbooks_Budget_Detail__c record.
Budget sync is one-directional: QuickBooks → Salesforce. The connector only reads Budgets — there is no Push / create / update path from Salesforce to QuickBooks, no Custom Field Mapping, and no reverse field mapping for this entity.
Budgets reach Salesforce through two paths:
|
Path |
Entry Point |
When It Runs |
|---|---|---|
|
Full Sync |
|
Admin clicks Budget on the Sync Data from QuickBooks screen of the Admin Setup Console (or a scheduled run) |
|
Webhook |
|
QBO sends a change notification for a |
Both paths use the same service-layer methods in WS_Quickbooks to query QBO and to convert the response into Salesforce records.
2. Component Architecture
Admin Setup Console (adminSetup LWC)
"Sync Data from QuickBooks" → Budget tile
│
▼
AdminSetupController.syncQuickbooksData('Budget', companyId)
│
▼
┌──────────────────────────────────────────┐ QuickBooks Online
│ BatchAllQBSFBudgetSync │ POST /services/apexrest/getUpdatesQB/*
│ execute(SchedulableContext) → count │ (entity name = "Budget")
│ start() → concurrency guard + offsets │ │
│ execute() → page of Budgets │ ▼
│ finish() → Last_Sync_Config__c │ ┌───────────────────────────────┐
└──────────────┬───────────────────────────┘ │ Webhook_QBChangeHandler │
│ │ groups ids under key 'budget'│
│ └──────────────┬────────────────┘
│ ▼
│ ┌───────────────────────────────┐
│ │ BatchQBSFBudgetWebhook │
│ │ start() → concurrency guard │
│ └──────────────┬────────────────┘
│ ▼
│ ┌───────────────────────────────┐
│ │ QuickbooksIntegrationHelper │
│ │ handleQBBudget() │
│ └──────────────┬────────────────┘
▼ ▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ WS_Quickbooks │
│ queryBudgetCount(whereClause) → select COUNT(*) from Budget │
│ queryBudget(oRequestWrapper) → select * from Budget ... │
│ createBudgetResponseWrapper(...) → QBO JSON → QBApiDataWrapper.Budget │
│ createBudgetRecords(...) → cm_finance__Quickbooks_Budget__c │
│ createBudgetDetailRecords(...) → cm_finance__Quickbooks_Budget_Detail__c│
└──────────────────────────────────────────────────────────────────────────────┘
│
▼
Data.upsurt / UpsertCustomRecords.upsertQBBudgetRecords()
UpsertCustomRecords.upsertQBBudgetDetailRecords()
3. QuickBooks API Usage
Budget uses the standard QBO Query endpoint (Query_Endpoint__c on cm_finance__Quickbooks_Credentials__c). No create, update or PDF endpoints are used.
|
Operation |
Method |
Query |
Used By |
|---|---|---|---|
|
Count Budgets |
GET |
|
|
|
Page of Budgets |
GET |
|
|
|
Budgets by Id |
GET |
|
|
All calls refresh the access token first (refreshAccessToken()), send minorversion from Minor_Version__c, and call checkForCredentialsUpdate() afterwards to persist a rotated refresh token.
3.1 Sample QBO Budget Response
{
"QueryResponse": {
"Budget": [
{
"Id": "QB_BUDGET_1",
"SyncToken": "0",
"Name": "FY26 Budget",
"StartDate": "2026-01-01",
"EndDate": "2026-12-31",
"BudgetType": "ProfitAndLoss",
"BudgetEntryType": "Monthly",
"Active": true,
"sparse": false,
"domain": "QBO",
"MetaData": {
"CreateTime": "2026-01-01T10:00:00-07:00",
"LastUpdatedTime": "2026-01-15T10:00:00-07:00"
},
"BudgetDetail": [
{ "Amount": 1000.50, "BudgetDate": "2026-01-01", "AccountRef": { "name": "Revenue", "value": "1" } },
{ "Amount": 500.25, "BudgetDate": "2026-02-01", "CustomerRef": { "name": "Acme Corp", "value": "2" } },
{ "Amount": 250.00, "BudgetDate": "2026-03-01", "ClassRef": { "name": "Consulting", "value": "3" } },
{ "Amount": 750.75, "BudgetDate": "2026-04-01", "DepartmentRef": { "name": "Sales", "value": "4" } }
]
}
]
}
}
4. Class Reference
4.1 BatchAllQBSFBudgetSync — Full Sync
File: force-app/main/default/classes/BatchAllQBSFBudgetSync.cls Access Modifier: global with sharing Implements: Database.Batchable<Object>, Database.AllowsCallouts, Schedulable, Database.Stateful
Pulls every Budget (optionally filtered) for one QuickBooks company, following the same count-first / paginate-in-chunks pattern as the other BatchAllQBSF*Sync classes.
|
Property |
Description |
|---|---|
|
|
QuickBooks Realm Id of the company to sync. Must be set before execution. |
|
|
Total count returned by the COUNT query; drives the size of the iterable in |
|
|
Running offset used to compute |
|
|
Read from |
execute(SchedulableContext sc) — Count & Dispatch
-
Sets
WS_Quickbooks.sCompanyIdandoCredentialsforcompanyId. -
Calls
WS_Quickbooks.queryBudgetCount(whereClause)and readsQueryResponse.totalCount. -
Reads the
BatchSizecommon setting (default50) and caps it at 10. -
Starts
new BatchAllQBSFBudgetSync(totalCount)with that batch size (skipped in tests).
Why the batch size is capped at 10: a single Budget can carry 200+
BudgetDetaillines. Each chunk upserts Budgets and all of their detail lines, so the number of Budgets per chunk is limited independently of the sharedBatchSizesetting to stay under the 10,000 DML rows per transaction limit.
start(Database.BatchableContext BC) — Concurrency Guard
Queries AsyncApexJob for another BatchAllQBSFBudgetSync in Processing status. If one is found it returns an empty list (the job exits without work). Otherwise it returns a placeholder list of budgetCount elements, so each chunk represents one page window.
execute(Database.BatchableContext BC, List<Object> lstRecords) — Page Processing
1. thisOffset = currentOffset; currentOffset += chunk size
2. Query: select * from Budget [whereClause] startposition {thisOffset + 1} maxresults {chunk size + 1}
3. WS_Quickbooks.queryBudget()
4. Resolve cm_finance__Quickbooks_Company__c by Company_ID__c = companyId
5. WS_Quickbooks.createBudgetRecords() → set Quickbooks_Company__c + Realm Id
6. Upsert Budgets on cm_finance__Composite_Unique_Key__c
7. Re-query Budgets by QuickBooks_Budget_Id__c + company → Map<QB Budget Id, SF Id>
8. WS_Quickbooks.createBudgetDetailRecords() → upsert on cm_finance__External_Key__c
9. Stamp Status__c on the Budgets and update them:
detail upsert OK → 'Successfully Synced'
detail upsert failed → 'Failed' + Status_Description__c = 'Budget Detail sync failed: <error>'
API errors, a missing company, or unexpected exceptions are written to the debug log only (no Failed record is created on this path).
finish(Database.BatchableContext BC)
Updates the Budget entry of cm_finance__Last_Sync_Config__c on the company record with lastSyncedAt = today and status = 'Completed'. This drives the last-sync date shown on the Budget tile of the Admin Setup Console.
Usage:
// Same call the Admin Setup Console makes
BatchAllQBSFBudgetSync job = new BatchAllQBSFBudgetSync();
job.companyId = '<QuickBooks Realm Id>';
job.execute(null); // runs the COUNT query and starts the batch
// Scheduling — companyId must be set on the instance before scheduling
BatchAllQBSFBudgetSync scheduled = new BatchAllQBSFBudgetSync();
scheduled.companyId = '<QuickBooks Realm Id>';
System.schedule('QB Budget Sync', '0 0 2 * * ?', scheduled);
4.2 BatchQBSFBudgetWebhook — Webhook Batch
File: force-app/main/default/classes/BatchQBSFBudgetWebhook.cls Access Modifier: public with sharing Implements: Database.Batchable<String>, Database.AllowsCallouts
Follows the identical pattern of the other BatchQBSF*Webhook classes.
|
Method |
Behaviour |
|---|---|
|
|
Primary constructor used by |
|
|
Concurrency guard on |
|
|
Creates |
|
|
Debug log only |
Webhook_QBChangeHandler dispatches this batch when the payload contains entities named Budget (lower-cased key budget) with operation create, update or void, using the BatchSize common setting as the batch size.
4.3 QuickbooksIntegrationHelper.handleQBBudget()
Step 1: Resolve company
→ cm_finance__Quickbooks_Company__c WHERE Company_ID__c = realmId AND Active__c = true
→ not found: debug log and return (no records written)
Step 2: Query QBO
→ SELECT * FROM Budget WHERE Id IN (<webhook ids>)
→ WS_Quickbooks.queryBudget()
Step 3: Budgets
→ WS_Quickbooks.createBudgetRecords()
→ set Quickbooks_Company__c, Realm Id, Composite_Unique_Key__c = {QB_Id}::Budget::{RealmId}
→ UpsertCustomRecords.upsertQBBudgetRecords()
Step 4: Budget Details
→ WS_Quickbooks.createBudgetDetailRecords(Map<QB Budget Id, SF Id>)
→ UpsertCustomRecords.upsertQBBudgetDetailRecords()
Step 5: Status
→ Budgets updated to 'Successfully Synced', Status_Description__c cleared
Failure handling:
|
Scenario |
Result on existing |
|---|---|
|
API call failed / no Budget returned |
|
|
Unhandled exception |
|
Only Budgets that already exist in Salesforce are marked as failed — a Budget that has never been synced has no record to update.
Echo suppression: the
Ignore_Webhook__cpattern used by other entities does not apply to Budget, because Salesforce never writes Budgets to QuickBooks. Every Budget webhook results in a live QBO query.
4.4 WS_Quickbooks — Budget Methods
|
Method |
Returns |
Purpose |
|---|---|---|
|
|
|
Runs |
|
|
|
Runs |
|
|
|
Parses one QBO Budget, including |
|
|
|
Maps wrappers to Salesforce Budget records (status |
|
|
|
Maps every |
|
(private) |
|
Converts QBO ISO-8601 timestamps with UTC offsets (e.g. |
queryBudget error messages (set on oAPIResponse.errorMessage):
|
Message |
Meaning |
|---|---|
|
|
|
|
|
Access token refresh failed |
|
|
HTTP 200 but no |
|
|
|
|
|
HTTP 200 with an empty body |
|
|
Non-200 response, parsed with |
|
|
Unexpected exception |
4.5 QBApiDataWrapper — Budget Classes
QBApiDataWrapper.Budget
|
Field |
Type |
QBO Source |
|---|---|---|
|
|
String |
|
|
|
String |
|
|
|
String |
|
|
|
Date |
|
|
|
String |
|
|
|
String |
|
|
|
Boolean |
|
|
|
Boolean |
|
|
|
String |
|
|
|
DateTime |
|
|
|
List<BudgetDetail> |
|
QBApiDataWrapper.BudgetDetail — amount (Decimal), budgetDate (Date), lstReferenceDetail (List<ReferenceDetail>).
QBApiDataWrapper.ReferenceDetail — sReferenceType (AccountRef, CustomerRef, ClassRef or DepartmentRef), sName, sValue (QBO Id).
QBResponseWrapper.lstBudgetResponse holds the parsed Budgets.
4.6 UpsertCustomRecords — Budget Methods
|
Method |
Target Object |
Upsert Key |
|---|---|---|
|
|
|
|
|
|
|
|
Both delegate to Data.upsurt() with FLS enforcement from CMQBConnectorUtility.enforceFLS.
5. Data Model
5.1 cm_finance__Quickbooks_Budget__c (Quickbooks Budget)
Record name: Auto Number QB-{000000} Relationships: Lookup to QuickBooks Company (Quickbooks_Company__c)
|
Field |
API Name |
Type |
Populated From |
|---|---|---|---|
|
Budget Name |
|
Text(255) |
|
|
QuickBooks Budget Id |
|
Text(255), External Id |
|
|
Composite Unique Key |
|
Text(255), External Id, Unique |
|
|
Sync Token |
|
Text(20) |
|
|
Budget Type |
|
Picklist: |
|
|
Budget Entry Type |
|
Picklist: |
|
|
Start Date |
|
Date |
|
|
End Date |
|
Date |
|
|
Active |
|
Checkbox |
|
|
Sparse |
|
Checkbox |
|
|
Domain |
|
Text(20) |
|
|
Created Time |
|
DateTime |
|
|
Last Updated Time |
|
DateTime |
|
|
Last Sync Date |
|
DateTime |
|
|
Quickbooks Company |
|
Lookup(Quickbooks Company) |
Company matched by Realm Id |
|
Quickbooks Company Realm Id |
|
Text(255) |
Realm Id |
|
Status |
|
Picklist |
See 5.3 |
|
Status (image) |
|
Formula (Text) |
Status icon rendered from |
|
Status Description |
|
Long Text Area |
Error details when a sync fails |
5.2 cm_finance__Quickbooks_Budget_Detail__c (Quickbooks Budget Detail)
Record name: Auto Number QBD-{000000} Relationships: Master-Detail to Quickbooks Budget (Quickbooks_Budget__c) — detail records are deleted with their Budget.
|
Field |
API Name |
Type |
Populated From |
|---|---|---|---|
|
Quickbooks Budget |
|
Master-Detail |
Parent Budget |
|
Amount |
|
Currency |
|
|
Budget Date |
|
Date |
|
|
Account Ref Id / Name |
|
Text(255) |
|
|
Customer Ref Id / Name |
|
Text(255) |
|
|
Class Ref Id / Name |
|
Text(255) |
|
|
Department Ref Id / Name |
|
Text(255) |
|
|
External Key |
|
Text(255), External Id |
Built by |
|
Name / Reference Type / Value |
|
Text / Picklist / Text |
Present on the object but not populated by the current sync |
The reference fields store the QBO Id and display name as text; they are not lookups to
cm_finance__Quickbooks_Account__c,cm_finance__Quickbooks_Customer__corcm_finance__Quickbooks_Class__c. Join onQuickbooks_Id__cin reports if needed.
5.3 Status Lifecycle
|
Status |
Set By |
|---|---|
|
|
Transient value set by |
|
|
Budget and all its detail lines upserted |
|
|
Full sync: detail upsert failed. Webhook: QBO API call failed |
|
|
Webhook: unhandled exception in |
|
|
Available in the picklist but not used (Budget is never pushed to QuickBooks) |
5.4 Key Formats
|
Record |
Key |
Format |
Example |
|---|---|---|---|
|
Budget |
|
|
|
|
Budget Detail |
|
|
|
Only the reference segments present on a line are included. Re-running a sync therefore updates existing lines instead of duplicating them.
6. Configuration Reference
|
Setting |
Location |
Default |
Effect on Budget Sync |
|---|---|---|---|
|
|
|
|
Full sync: capped at 10 Budgets per chunk. Webhook: used as-is |
|
|
|
(record not shipped — all Budgets synced) |
Optional QBO WHERE clause appended to the COUNT and page queries, e.g. |
|
|
|
|
User types that bypass FLS enforcement (webhook runs as the Site Guest User) |
Note on the metadata record name:
CMQBConnectorUtility.getWhereClause('AllBudget')looks for the DeveloperNameBatchAllQBSFAllBudgetSync_WhereClause(note the extraAll), notBatchAllQBSFBudgetSync_WhereClause. Create the record with exactly that name to filter the full sync.
6.1 Site Guest User Access (Webhook)
Budget webhooks are processed as the Site Guest User. In addition to the steps in Post Installation Steps, grant the guest user profile:
-
Apex Class Access:
cm_finance.BatchQBSFBudgetWebhook -
Object Settings:
cm_finance__Quickbooks_Budget__candcm_finance__Quickbooks_Budget_Detail__c— Read, Create, Edit, View All Fields; field Read/Edit on all fields
6.2 Internal Users
Grant users who need to view Budgets Read access to both objects and their fields (via the CM QuickBooks Connector permission sets or a separate permission set), and add a Quickbooks Budgets tab and the Quickbooks Budget Details related list to the Budget page layout as required.
7. End-to-End Flow Diagrams
7.1 Full Sync
Admin clicks "Budget" on Sync Data from QuickBooks
│
▼
AdminSetupController.syncQuickbooksData('Budget', realmId)
│
▼
BatchAllQBSFBudgetSync.execute(null)
├── queryBudgetCount() → totalCount = 25
├── batchSize = min(BatchSize, 10) = 10
└── Database.executeBatch(new BatchAllQBSFBudgetSync(25), 10)
│
├── start(): another run Processing? → yes: exit / no: 25 placeholders
│
├── execute() chunk 1 → startposition 1, Budgets 1–10
├── execute() chunk 2 → startposition 11, Budgets 11–20
├── execute() chunk 3 → startposition 21, Budgets 21–25
│ each chunk: upsert Budgets → upsert Details → set Status
│
└── finish(): Last_Sync_Config__c.Budget = {lastSyncedAt: today, status: Completed}
7.2 Webhook
User edits a Budget in QuickBooks Online
│
▼
QBO → POST /services/apexrest/getUpdatesQB/ (Header: RealmId)
{ "name": "Budget", "id": "12", "operation": "Update" }
│
▼
Webhook_QBChangeHandler → mapEntitiesData = { "budget": ["12"] }
→ Database.executeBatch(new BatchQBSFBudgetWebhook(["12"], realmId), BatchSize)
→ HTTP 200
│
▼
BatchQBSFBudgetWebhook.execute() → QuickbooksIntegrationHelper.handleQBBudget()
├── SELECT * FROM Budget WHERE Id IN ('12')
├── Upsert Budget on 12::Budget::{RealmId}
├── Upsert Details on External_Key__c
└── Status = 'Successfully Synced'
8. Known Limitations & Design Notes
|
Item |
Detail |
|---|---|
|
Read-only entity |
No Salesforce → QuickBooks path; edit Budgets in QuickBooks Online |
|
Removed lines are not cleaned up |
Detail lines are upserted by |
|
Detail key has no Realm Id |
|
|
Full sync errors are log-only |
API failures, missing company, and exceptions in |
|
Webhook batch size is not capped |
The 10-Budget cap applies to the full sync only; a webhook delivering many large Budgets at once uses |
|
Concurrency guard is per class |
A second full sync (even for another company) exits immediately while one is |
|
Unused fields |
|
9. Test Classes
|
Test Class |
Covers |
|---|---|
|
|
|
|
|
Full sync: count/dispatch, concurrency guard, page processing, |
|
|
Webhook batch and |
|
|
|