You ship a custom Agentforce action that returns a suitability disclosure alongside a portfolio recommendation. Through two weeks of UAT it renders as the same card every time. Then in production it comes back as a paragraph, then as a bulleted list, then — once — not at all, because the model decided the summary text already covered it. The defect is not a wrong answer. It is a correct answer presented differently each time, and when a regulator depends on that presentation, you have a compliance problem you cannot reproduce on demand.
The hundredth conversation is the one you fail
A builder can write "always display the suitability disclosure with any portfolio recommendation" into the instructions, and the agent will do it. It will probably do it in 99 interactions out of 100. Stochastic reasoning means the hundredth is not a bug you can find by testing harder — it is a property of the system. The two regulated cases Salesforce uses to make this point will be familiar: a financial services firm that must attach a suitability disclosure to every portfolio recommendation, and a healthcare agent that must prove consent was captured before a procedure was scheduled.
Ninety-nine percent is the worst possible number here. Zero percent fails in the first demo and gets fixed that afternoon. Ninety-nine percent passes UAT, passes the pilot, passes the first quarter, and then surfaces as one session an auditor pulled at random. I have seen a release sign-off rest entirely on "we ran it forty times and it always showed the card" — which is evidence of nothing, because the failure mode is defined by its rarity.
So, the position I would argue for: put the deterministic boundary at the action output, soql-not-in-not-equal-exclusion/" class="auto-link">not in the prompt. Anything a regulator, a contract or a form-completion flow depends on gets bound to the action that produces it. Everything else stays stochastic, because that is where the model earns its keep.
Two axes get conflated here and shouldn't. Salesforce publishes a levels-of-determinism framework for Agentforce with six progressive levels: instruction-free subagent and prompt action selection (1), agent instructions (2), data grounding (3), agent variables (4), deterministic actions in Flow, Apex or APIs (5), and Agent Script (6) with hard-coded reasoning logic, if/else branching, mandatory authentication gates and forced subagent transitions. That ladder governs what the agent does. Deterministic rendering governs how the result appears. Getting to Level 5 with an airtight Apex action buys you nothing if the model then paraphrases its output as prose. You need both — and the framework's own caution applies to both: escalate for guaranteed execution order or verbatim disclosures, but "do not overscript your agents down to the level that they become glorified chatbots."
What the render: directive actually changes
The mechanism is simple, and the simplicity is the point. A builder associates an action with a specific response format for a particular connection. When that action runs, the Agentforce Connections layer uses the configured response format instead of asking the LLM to select the UX component. Nothing about the model became more predictable. The decision was taken away from it.
"For a particular connection" is the qualifier worth reading twice. The binding is scoped to a connection, so agent behaviour can still be customised per connection everywhere else. You are removing one choice in one place, not freezing the experience.
Response formats are also why this is not a Lightning-only feature. A response format can describe the structured JSON an external experience expects, so a custom React front end, a third-party interface or a headless API channel can be bound the same way an LWC is. The Connections layer already owns rich agent experiences across web chat, WhatsApp, SMS, phone, kiosks and other channels — including agent behaviour customisation, response customisation, multi-channel rendering and structured JSON for headless scenarios — and deterministic rendering rides on that.
One honesty note on naming: the source material introduces this as the render: directive and later refers to render_as, and Salesforce has not published reference documentation fixing the exact configuration key. Treat the concept as settled and the key as not yet confirmed. I am not going to show a config snippet for it, because any syntax I wrote would be a guess.
The binding you can wire up today
There is an adjacent, documented mechanism that covers Lightning surfaces now: Custom Lightning Types, introduced in Summer '25. It is not the render: directive. It is a type-level binding between the Apex request/response class on your action and an LWC of yours, and it applies to actions that use Apex classes for their requests and responses.
Take a consent receipt returned by an action that schedules a procedure. The Apex response class exposes its fields with @InvocableVariable:
public with sharing class ProcedureConsentReceipt {
@InvocableVariable(label='Procedure' required=true)
public String procedureName;
@InvocableVariable(label='Consent captured at' required=true)
public Datetime consentCapturedAt;
@InvocableVariable(label='Attested by' required=true)
public String attestedBy;
@InvocableVariable(label='Clauses acknowledged')
public List<ConsentClause> clauses;
public class ConsentClause {
@InvocableVariable public String clauseLabel;
@InvocableVariable public String clauseVersion;
@InvocableVariable public Boolean acknowledged;
}
}
The metadata lives in its own tree in the DX package:
force-app/main/default/lightningTypes/
├── procedureConsentReceipt/
│ ├── schema.json
│ └── lightningDesktopGenAi/
│ └── renderer.json
└── consentClause/
├── schema.json
└── lightningDesktopGenAi/
└── renderer.json
schema.json points at the Apex type and carries the human-readable metadata:
{
"title": "Procedure consent receipt",
"description": "Consent captured before a procedure was scheduled.",
"lightning:type": "@apexClassType/c__ProcedureConsentReceipt"
}
Inner classes are referenced with $, so the clause type's schema uses "lightning:type": "@apexClassType/c__ProcedureConsentReceipt$ConsentClause".
renderer.json maps the type to a component. The $ key means "the whole custom type":
{
"renderer": {
"componentOverrides": {
"$": { "definition": "c/procedureConsentReceipt" }
}
}
}
For a collection, the override nests under a collection key. This is also where you reconcile a property-name mismatch: the left-hand side is your LWC property, {!$attrs} points at the Apex class, and the right-hand side is the source property on it.
{
"collection": {
"renderer": {
"componentOverrides": {
"$": {
"definition": "c/consentClauseRow",
"attributes": {
"heading": "{!$attrs.clauseLabel}",
"versionTag": "{!$attrs.clauseVersion}"
}
}
}
}
}
}
The component declares the Agentforce output target:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<isExposed>true</isExposed>
<targets>
<target>lightning__AgentforceOutput</target>
</targets>
</LightningComponentBundle>
For an input override you use editor.json and the lightning__AgentforceInput target instead. Each channel gets its own folder in place of lightningDesktopGenAi — enhancedWebChat, for example — which is the part that quietly doubles the work.
| Approach | What guarantees the render | Channel coverage | What you maintain | Audit evidence |
|---|---|---|---|---|
| Prompt instruction | Nothing. The model complies most of the time | Every channel, equally unreliably | Instruction text | Transcript only |
| Custom Lightning Type binding | Apex type bound to a component, for actions using Apex request/response classes | One renderer folder per channel (lightningDesktopGenAi, enhancedWebChat, …) |
The LWC, schema.json and renderer.json per channel |
Not documented as a distinct render event |
Response format + render: directive |
Connections layer applies the configured format instead of asking the LLM | Connections channels: web chat, WhatsApp, SMS, phone, kiosks, headless JSON | The response format plus the component or external consumer | Per-action, per-session event log entries |
One turn, two actions, two different guarantees
Multi-action turns are where this gets sharp. When several actions execute in one interaction and only one of them is configured for deterministic rendering, the platform has to identify that specific action, attach the correct UX component to its output, and let the remaining actions continue through the non-deterministic path. The illustration in the source is a chained pair: an agent finds top restaurants in a location, then retrieves the menu from the top result.
The developer consequence is that a single reply can contain one guaranteed component and one improvised one. If your disclosure logic is spread across two actions and you bound one of them, you have proved half of it. Design action granularity around what must be provable: if a disclosure and the recommendation it qualifies have to appear together, make them one action returning one response object rather than two actions the platform is free to render differently.
Supervised and long-running actions add a timing rule. The component renders only after the corresponding action has been approved and executed — otherwise the UX could appear before the underlying action completes. That is the right behaviour and an awkward one to design around. Do not build a form that is supposed to collect input while an approval is pending; it will not be on screen. Split it instead: one action that renders the pre-approval summary and captures whatever you need up front, then the supervised action, then a bound post-execution component showing the outcome. If the user needs something to look at during the wait, it has to come from the agent's text, not the bound component.
The log entry is the deliverable
Deterministic rendering events are instrumented in Agentforce event logs at the per-action, per-session level. Each event captures which action fired, which response format was applied, which connection rendered it, and at what point in the conversation.
That instrumentation is the commercially interesting half. A compliance officer can query the logs to verify that a specific disclosure rendered in a specific session, or run aggregate reports showing rendering rates across all interactions over a period. Platform-enforced rendering plus a per-event audit trail is a guarantee you can demonstrate to an auditor, which prompt engineering never could — "we asked the model firmly" is not evidence.
Practical consequence for your build: agree the exact question compliance will ask before you write the action, then name the action and its response format so the answer is one filter away. "Show me every session last quarter where a recommendation went out without the disclosure render event" is answerable when action naming is consistent across teams and unanswerable when three squads each invented their own.
What to watch for
- The configuration key is not yet confirmed. The source uses both
render:andrender_as, and no reference documentation pins it down. Do not build tooling around a guessed key. - Every binding is a component you now own — versioned, regression-tested, and tested again per channel. Each channel has its own renderer folder, and nothing stops the desktop and web chat variants from drifting apart over a couple of releases.
- You give up adaptability exactly where you bind. A bound component renders the same way when the conversation goes somewhere you did not plan for. That is the trade, and it is only worth paying where you need proof.
- Mixed turns render mixed. One bound action in a chain guarantees one component, not the whole reply.
- Supervised actions render late by design. Approval and execution come first, always.
- Custom Lightning Types apply to actions that use Apex classes for requests and responses. An action built another way does not get this binding.
- Whether Custom Lightning Type overrides emit their own audit events is not documented. Do not assume the per-action event trail described for Connections-layer response formats covers them.
Leave a Comment