Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
Diagram showing Agentforce deterministic UI rendering binding an action response to a fixed component
Agentforce & AI

Agentforce deterministic UI rendering: bind it to actions

An Agentforce action that shows the disclosure 99 times out of 100 passes UAT and fails the audit. Here is how Agentforce deterministic UI rendering moves the choice from the LLM to the Connections layer, what you can bind today with Custom Lightning Types, and where the trade-offs bite.

The short answer

Agentforce removes the rendering decision from the LLM: a builder associates an action with a response format for a connection, and the Connections layer applies that format instead of letting the model pick a component. Every render is logged per action and per session, so you can prove a disclosure appeared.

Key takeaways Put the deterministic boundary at the action output, not in the prompt — bind only what a regulator, contract or form flow depends on, and leave the rest stochastic. Design action granularity around provability: a disclosure and the recommendation it qualifies belong in one action returning one response object, because a mixed turn renders one guaranteed component and one improvised one. Use Custom Lightning Types (introduced in Summer '25) today to bind an LWC to an Apex request/response class via schema.json and renderer.json, and budget for one renderer folder per channel. Never design a form that collects input while a supervised action is pending — the bound component only renders after approval and execution. Agree the exact compliance query before you build, then name actions and response formats so the per-action, per-session event log answers it with one filter.

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 lightningDesktopGenAienhancedWebChat, 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: and render_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.

Originally reported by engineering.salesforce.com

Frequently asked questions

Can I just tell the Agentforce prompt to always show the disclosure?

You can, and it will mostly work. Stochastic reasoning means the agent may follow that instruction in 99 out of 100 interactions and skip it in the hundredth, which is exactly the failure profile that passes UAT and fails an audit.

What is the difference between Custom Lightning Types and the render directive in Agentforce?

Custom Lightning Types are a documented binding between an Apex request/response class on your action and one of your LWCs, configured per channel folder. The render: directive is the Connections-layer mechanism that associates an action with a response format for a connection so the platform, not the LLM, decides the presentation — and it carries the per-action event logging.

Does Agentforce deterministic UI rendering only work with Lightning Web Components?

No. Response formats are the abstraction, so you can define the structured JSON an external experience expects — a custom React app, a third-party interface, or a headless API channel — and associate that response format with the render: directive. The same model applies whether the output becomes an LWC or is consumed as JSON.

Why does my bound component not render until the Agentforce action is approved?

That is deliberate. For long-running or supervised actions the component renders only after the corresponding action has been approved and executed, so the UX cannot show up before the underlying work completes. Split pre-approval capture and post-execution confirmation into separate actions.

Is the Agentforce configuration key render: or render_as?

Not yet confirmed. The available source material uses both names for the same mechanism and Salesforce has not published reference documentation fixing the key, so do not hard-code either one into a deployment script yet.

Newsletter

One email every Tuesday

New guides, tool updates, and the release-note changes that break things.

No spam. Unsubscribe in one click.

Comments

Loading comments...

Leave a Comment