A practical guide for Salesforce developers building JSON payloads, emails, SOQL, and integration code
Introduction
If you’ve spent any real time writing Apex, you’ve written this code before – a giant string built out of dozens of + concatenations, half of them wrapped just to keep the line length sane, with quotes escaped everywhere and no easy way to tell where one field ends and the next begins. Building HTTP request bodies, email templates, dynamic SOQL, or JSON payloads in Apex has always meant fighting the language a little.
Summer ’26 finally fixes this. Apex now supports multiline strings using triple-quote syntax, along with a new String.template() method for clean, named-variable interpolation. It’s a quality-of-life release, not a governor-limit or architecture change, but it’s the kind of feature that quietly saves hours across every integration class, trigger handler, and email service you maintain.
This post walks through what’s new, how it works, where it shines, and where the old patterns still make sense.
The Problem This Solves
Consider a typical integration class building a JSON payload for an outbound callout:
String payload = '{' +
'"accountName": "' + acc.Name + '",' +
'"accountId": "' + acc.Id + '",' +
'"lastUpdated": "' + String.valueOf(Datetime.now()) + '"' +
'}';
It works, but it’s brittle. Miss a comma or a quote and you’ve got a runtime error that’s annoying to trace. Now imagine that same pattern for a multi-field email body or a 40-line SOQL query assembled dynamically – readability falls apart fast, and code review becomes a search for missing parentheses.
String.format() helped a little by letting you use {0}, {1} placeholders, but it introduced its own problem: positional indexes. Add a new field in the middle of the list and every subsequent index shifts, silently breaking the output if you forget to update them.
Summer ’26 addresses both pain points at once.
Multiline Strings: Triple-Quote Syntax
You can now declare a string that spans multiple lines using triple single quotes ('''), without any concatenation at all:
String query = '''
SELECT Id, Name, StageName, CloseDate, Account.Name, Owner.Name
FROM Opportunity
WHERE StageName = 'Closed Won'
AND CloseDate = THIS_QUARTER
ORDER BY CloseDate DESC
''';
List<Opportunity> opps = Database.query(query);
A few things worth knowing:
- This feature is available across all API versions, so you don’t need to bump your org’s API version to start using it.
- It works the same way in both Lightning Experience and Salesforce Classic, and across Enterprise, Performance, Unlimited, and Developer editions.
- Line breaks and indentation inside the triple quotes are preserved as part of the string, so formatting your source code for readability also formats the resulting string – handy for SOQL and JSON, less handy if you’re not careful with leading whitespace on each line.
This alone is a nice win for dynamic SOQL, HTML email bodies, and any place you were previously gluing strings together with + just to keep them on separate lines in your IDE.
String.template(): Named Placeholders Instead of Indexes
The bigger change is the new String.template() instance method, which replaces index-based interpolation with named placeholders using ${variableName} syntax. You call .template() directly on a string (multiline or not) and pass in a Map<String, Object> of values to substitute.
String message = '''
Hello ${firstName},
Your order was dispatched on ${dispatchDate}.
Your tracking reference is ${trackingRef}.
'''.template(new Map<String, Object>{
'firstName' => 'Uday',
'dispatchDate' => Date.today(),
'trackingRef' => 'TRK-20260701'
});
Compare that to the equivalent String.format() call, where you’d need to remember that {0} is the first name, {2} is the tracking reference, and so on – and re-verify every index if you ever reorder the arguments. With String.template(), the map keys are self-documenting. Anyone reading the code – or reviewing the pull request – can match a placeholder to its value without cross-referencing an index list.
It’s a small syntactic change, but it removes an entire category of “off-by-one” bugs that show up when templates get edited months after they were written.
Real-World Use Cases
1. JSON Payloads for Integrations
This is probably the single biggest win for anyone doing REST callouts – MDM sync jobs, middleware integrations, or anything hitting an external API.
public String buildAccountPayload(Account acc) {
return '''
{
"accountId": "${accountId}",
"accountName": "${accountName}",
"lastModified": "${lastModified}"
}
'''.template(new Map<String, Object>{
'accountId' => acc.Id,
'accountName' => acc.Name,
'lastModified' => String.valueOf(acc.LastModifiedDate)
});
}
There’s a subtler win buried in here too: if a payload needs a field named after an Apex reserved word – type, class, or currency, for example – you previously couldn’t build that structure using a wrapper class, because Apex won’t let you name a class property type or class. Multiline strings sidestep the problem entirely, since you’re building raw text rather than serializing an object.
2. Email Bodies
Email templates built in Apex tend to accumulate string concatenation faster than almost anything else in a codebase. String.template() cleans this up directly:
String body = '''
Hi ${name},
Your case ${caseNumber} has been updated to status: ${status}.
Regards,
Support Team
'''.template(new Map<String, Object>{
'name' => contact.FirstName,
'caseNumber' => cs.CaseNumber,
'status' => cs.Status
});
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setPlainTextBody(body);
email.setToAddresses(new List<String>{ contact.Email });
Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{ email });
3. Dynamic SOQL
Long, conditionally-built SOQL queries are far easier to read and maintain when written as a single multiline block instead of a chain of concatenated fragments.
4. Debug Logs and HTTP Request Bodies
Anywhere you’re currently building a debug string with a dozen + operators to log variable state, or assembling an HTTP request body by hand, both features apply directly.
More Integration Examples
Integration code is where this feature earns its keep the most – outbound callouts, inbound payload parsing, error logging, and batch sync jobs all involve building or handling large blocks of text. Here are a few patterns that come up constantly in MDM, ERP, and middleware integrations.
5. Outbound REST Callout with Nested Fields
Real integration payloads are rarely flat. Multiline strings make nested JSON readable without a wrapper class or JSON.serialize() gymnastics – especially useful when the target system’s field names don’t map cleanly to Apex-safe property names.
public String buildGeneratorPayload(Account generatorAcc) {
return '''
{
"generatorId": "${generatorId}",
"epaId": "${epaId}",
"status": "${status}",
"address": {
"line1": "${addressLine1}",
"city": "${city}",
"state": "${state}",
"postalCode": "${postalCode}"
},
"lastSyncedAt": "${syncTimestamp}"
}
'''.template(new Map<String, Object>{
'generatorId' => generatorAcc.Id,
'epaId' => generatorAcc.EPA_ID__c,
'status' => generatorAcc.Status__c,
'addressLine1' => generatorAcc.BillingStreet,
'city' => generatorAcc.BillingCity,
'state' => generatorAcc.BillingState,
'postalCode' => generatorAcc.BillingPostalCode,
'syncTimestamp' => String.valueOf(Datetime.now())
});
}
6. Building the HTTP Request Itself
The same pattern extends naturally to constructing the callout:
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:MDM_API/generators');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(buildGeneratorPayload(generatorAcc));
Http http = new Http();
HttpResponse res = http.send(req);
Because the payload-building logic is isolated in its own method using a template, it’s trivial to unit test independently of the actual callout – just assert on the returned string.
7. Structured Error Logging for Failed Callouts
Integration error logs are usually one of the messiest string-concatenation offenders in a codebase, since they tend to accumulate fields over time (status code, endpoint, payload snippet, correlation ID). A template keeps this readable even as fields get added:
String errorLog = '''
Integration Failure
--------------------
Endpoint: ${endpoint}
Status Code: ${statusCode}
Correlation Id: ${correlationId}
Timestamp: ${timestamp}
Response Body: ${responseBody}
'''.template(new Map<String, Object>{
'endpoint' => req.getEndpoint(),
'statusCode' => String.valueOf(res.getStatusCode()),
'correlationId' => correlationId,
'timestamp' => String.valueOf(Datetime.now()),
'responseBody' => res.getBody()
});
Integration_Log__c logRecord = new Integration_Log__c(
Message__c = errorLog,
Status__c = 'Failed'
);
insert logRecord;
8. Platform Event Payloads (Kafka / MDM Sync)
For architectures publishing Platform Events into a Kafka pipeline – a common pattern for Oracle MDM sync – the event payload is often a JSON string stored in a Long Text Area field on the event itself. Templates make this construction consistent across every publisher in the codebase:
public String buildGeneratorSyncEvent(Id recordId, String changeType) {
return '''
{
"eventType": "${eventType}",
"sourceSystem": "Salesforce",
"recordId": "${recordId}",
"changeType": "${changeType}",
"publishedAt": "${publishedAt}"
}
'''.template(new Map<String, Object>{
'eventType' => 'Generator_Sync',
'recordId' => recordId,
'changeType' => changeType,
'publishedAt' => String.valueOf(Datetime.now())
});
}
Generator_Sync_Event__e evt = new Generator_Sync_Event__e(
Payload__c = buildGeneratorSyncEvent(gen.Id, 'UPDATE')
);
EventBus.publish(evt);
9. Batch Callout Bodies for Mass Updates
When sending mass updates in a single batched request – for example, a nightly Generator record sync – you can loop through records and append templated JSON fragments into an array, keeping each fragment’s construction clean even though the overall payload is assembled iteratively:
List<String> records = new List<String>();
for (Account gen : generatorsToSync) {
records.add('''
{
"id": "${id}",
"epaId": "${epaId}",
"status": "${status}"
}
'''.template(new Map<String, Object>{
'id' => gen.Id,
'epaId' => gen.EPA_ID__c,
'status' => gen.Status__c
}));
}
String batchPayload = '[' + String.join(records, ',') + ']';
Note that the outer array wrapping still uses simple concatenation here – templates handle the shape of each JSON object cleanly, but joining a variable-length list of objects into an array is still best done with String.join().
Things to Watch For
- Reserved word collisions in maps still need care. The map keys are just strings, so there’s no compile-time checking that a
${placeholder}in your template actually has a matching key in the map – a typo in either place will pass compilation and fail (or silently produce wrong output) at runtime. Unit test your templates the same way you would any other string-building logic. - Whitespace is literal. Because indentation inside triple-quoted strings is preserved, heavily indented code can produce oddly-indented output (extra spaces in emails or JSON). Keep an eye on this if your team enforces consistent code indentation.
- This isn’t a security or governor-limit change. It ships in the same Summer ’26 release as a much bigger shift – Apex code compiled at API version 67.0 runs database operations in user mode by default instead of system mode – but the two features are unrelated. Don’t assume adopting multiline strings requires touching your API version, and don’t assume upgrading your API version is “just” about strings.
Should You Refactor Existing Code?
Not urgently, and not everywhere. Multiline strings and String.template() are additive – existing String.format() and concatenation-based code keeps working exactly as before. The pragmatic approach:
- Use it in all new code going forward – integration classes, email services, dynamic SOQL builders.
- Refactor opportunistically when you’re already touching a class for other reasons (bug fix, enhancement) and it contains a messy concatenation block.
- Don’t do a mass find-and-replace across a production codebase just to adopt new syntax. It adds regression risk for a purely cosmetic gain in already-stable code.
Conclusion
Multiline strings and String.template() won’t change how Apex executes, but they change how much friction there is in writing and reviewing it. If you build integration payloads, email templates, or dynamic SOQL – which is to say, if you write Apex at all – this is one of those Summer ’26 features that pays for itself the first week you use it. Pair it with a quick team norm (“new string-building code uses templates, not concatenation”) and you’ll notice the difference in code review within a sprint or two.
