Context and custom variables in Agentforce: how to stop your agent re-asking what it already knows
Your agent verified the customer, pulled their account, and then — two turns later — asked for their email again. That's not a model problem, it's a variables problem. Here's the difference between context, session, and custom variables, how to pass state deterministically between actions, and the pre-chat trap that ships an impersonation hole.
Here’s a demo that goes wrong in the room. The agent greets a verified customer by name, pulls their open cases, sounds sharp — and then, three turns later, asks for the email address it verified ninety seconds ago. Everyone watching quietly downgrades their estimate of how smart the thing is, and they’re right to. An assistant that forgets what it already knows isn’t an assistant; it’s a form with a personality.
The instinct is to blame the model, or to stuff more into the instructions: “Remember the customer’s ID throughout the conversation.” Both are wrong. A large language model is a bad place to store a fact you need to be exactly right every time, and an instruction is a suggestion, not a guarantee. The right place is a variable — a named slot that carries state across the conversation deterministically, so the customer ID captured by one action is there, unchanged, when the next action needs it. Variables are how an Agentforce agent stops re-asking, stops guessing, and starts behaving like one continuous session instead of a sequence of amnesiac turns.
This is also one of the more confusing corners of the platform, because Salesforce uses three overlapping words — context, session, and custom variables — and they don’t mean what a casual read suggests. This post untangles the taxonomy, shows how to pass state between actions the way the verify-then-book pattern needs, and covers the two things that bite in production: the read-only rule that surprises everyone, and the pre-chat variable pattern that ships an impersonation hole if you trust it.
The taxonomy: two real types and one UI label
Cut through the vocabulary and there are two actual kinds of variable, plus a piece of Builder UI that shares a name with neither.
Context variables are system-provided and prefixed with $Context. They hold information about the user and the session — and they’re read-only by default. They map to fields on the session records the platform already maintains; for a messaging Service Agent, that’s the MessagingSession record, exposing things like the end-user ID, the contact, and the routable session ID. You don’t create these; the platform hands them to you.
Custom variables (the metadata calls them conversation variables) are the ones you define. They’re read/write, they persist across turns, and their entire job is to move a value from one place in the conversation to another — most often, the output of one action into the input of a later action. This is the type that solves the “re-asking” problem.
“Session Variables” is neither a third type nor a trap you need to fear — it’s the name of a section in the Agent Builder Context tab where the MessagingSession-mapped context variables live. When a tutorial says “add a session variable,” it means “go to the Context tab, into the Session Variables section, and expose some MessagingSession fields to the agent.” Same machinery, different label.
Two types, one rule of thumb: context variables are what the platform knows about the session and hands you read-only; custom variables are what you decide to remember and pass along. Reach for a custom variable the moment a value produced in one step has to survive into a later one.
One naming detail that causes silent failures: when you add a custom field to the Messaging Session object, it becomes available as a context variable with the __c suffix dropped. Conversation_Key__c is referenced as $Context.Conversation_Key. Reference it with the __c and it simply won’t match — no error, no value, just an agent that behaves as if the field doesn’t exist.
Passing state between actions: the verify-then-book pattern
The canonical use of a custom variable is the one that fixes our opening demo. Take an agent that has to verify a customer before it books anything. Without variables, the verification action confirms identity, and then the booking action — a separate action, a separate turn — has no idea who the customer is, so the agent re-asks. With a custom variable, the identity flows through.
The wiring in Agent Builder looks like this:
1. Topic "Account Management" → Action "Verify Customer"
Output: customerId
→ On the output row, click "Assign to Variable"
→ Create/select a custom variable: VerifiedCustomerId
2. Same topic → Action "Create Booking"
Input: customerId
→ On the input, choose the variable → VerifiedCustomerId
The value flows in deterministically. The model never re-asks.
The important shift here is who is responsible for remembering the ID. It isn’t the model reasoning over the transcript — it’s a variable binding you configured. That’s the difference between “the agent usually carries the ID forward” and “the agent always does.” If you’ve built custom Apex actions, this is the layer above them: the action declares its inputs and outputs with @InvocableVariable, and variable mapping is how you wire one action’s output into the next action’s input across the conversation.
Filters: turning a variable into a guardrail
Variables don’t just carry values; paired with filters, they gate behavior. A filter is a condition over a variable that must be true before a topic or action runs. In the verify-then-book agent, you don’t just want to pass the customer ID — you want to make it impossible to book for an unverified customer. So you set a Boolean variable (IsVerified) when verification succeeds, and put a filter on the booking action: run only when IsVerified == true.
This matters more than it looks, and it’s the same principle that runs through everything we write about agent security: the model is not a trust boundary. An instruction that says “only book for verified customers” is a probabilistic hope. A filter that checks IsVerified == true is a deterministic gate the runtime enforces before the action can fire. When a decision is policy rather than judgment — identity checks, entitlement gates, anything with a compliance shadow — express it as a variable and a filter, not a sentence in the prompt. It’s the same deterministic-versus-probabilistic split we draw for routing in multi-agent orchestration: pin down what must never vary, leave the model free where variance is the point.
Using variables in instructions — and knowing what that does and doesn’t buy you
Variables also surface inside instructions, using merge syntax. The cleanest example is language:
Always respond to the customer in {!$Context.EndUserLanguage} language.
At runtime that resolves to the session’s actual language, so one instruction handles every locale. You can reference custom variables the same way — {!VerifiedCustomerId} — to give the model context about what it already knows.
But be precise about what this achieves. A value dropped into an instruction influences the model; it doesn’t constrain it. The model can still ignore it, misuse it, or narrate around it. That’s the crucial line: variables used as action inputs and filters are deterministic; variables shown in instructions are only influential. For anything that has to be exactly right — an ID passed to a system of record, a gate on a sensitive action — use the input/filter path. Save the instruction path for softening tone and behavior, where “usually right” is fine.
Passing variables in through the Agent API
When you drive an agent from your own backend rather than a Salesforce channel — the headless Agent API — you pass variables in the request. Both starting a session and sending a message accept a variables list, each entry a name, type, and value:
POST /einstein/ai-agent/v1/agents/{agentId}/sessions
{
"externalSessionKey": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"variables": [
{ "name": "$Context.EndUserLanguage", "type": "Text", "value": "en_US" },
{ "name": "VerifiedCustomerId", "type": "Text", "value": "003XXXXXXXXXXXX" }
]
}
(Treat the exact envelope as illustrative and confirm the current request shape in the developer docs — the load-bearing facts are that each variable is a name/type/value triple and where they’re allowed.) Two rules decide whether your value actually lands:
- Read-only context variables can only be set at the start of the session. You can seed
$Contextvalues on the Start Session call, but you can’t change them mid-conversation on a Send Message call — with one documented exception,$Context.EndUserLanguage, which is editable so an agent can switch languages when a caller does. - The variable must be flagged to allow it. In Builder, a variable only accepts an API-supplied value if “Allow value to be set by API” is checked. Miss that box and your value is silently ignored — one of the most common “I passed it and nothing happened” tickets.
Pre-chat variables, and the impersonation hole hiding in them
On web and in-app messaging, you can start the agent already-grounded by passing context from a pre-chat step — a known account ID, a logged-in user’s identity — so the agent doesn’t open cold. The mechanism: set hidden pre-chat fields on the embedded deployment, map them in the Omni-Channel flow onto the Messaging Session record, and let the agent read them through the Messaging Session context variable. This is the channel-level counterpart to the routing-and-context wiring we cover for messaging channels.
window.addEventListener("onEmbeddedMessagingReady", () => {
embeddedservice_bootstrap.prechatAPI.setHiddenPrechatFields({
"SourcePage": window.location.pathname,
// NOT a raw account id in the clear — see below
"SecureUserContext": encryptedSignedPayload
});
});
Here is the part that decides whether you’ve built a feature or a vulnerability: hidden pre-chat fields are client-side, and anything the browser sends, the user can forge. “Hidden” means hidden from the chat UI, not hidden from the person who opens dev tools. If you drop a raw AccountId into a hidden field and let the agent trust it, you’ve built impersonation-as-a-service — anyone can hand your agent someone else’s account number and be treated as them.
The safe pattern is to treat the pre-chat payload as untrusted input that has to be validated server-side. Send an encrypted, signed token rather than raw identifiers, and have the Omni-Channel flow call an Apex method that decrypts it, recomputes the signature, and re-checks the identity before writing a verified ID onto the session. Only the server-validated value ever gates a sensitive action — and the raw secrets never live in a variable the model can see. This is the same least-privilege discipline that applies to every agent consuming input it didn’t generate: the boundary is the server, not the prompt.
The gotchas that eat an afternoon
Five failure modes recur, and none are subtle once you know them:
- “My value won’t set via API.” The variable’s “Allow value to be set by API” box is unchecked, or you’re trying to change a read-only
$Contextvariable on a Send Message call instead of at session start. Only$Context.EndUserLanguageis editable mid-session. - The
__csuffix. Custom Messaging Session fields are referenced without__c.$Context.Conversation_Key, never$Context.Conversation_Key__c. Wrong form, silent no-match. - Looking for a “create variable” button. Custom/conversation variables are typically instantiated by mapping an action’s output to a new variable (“Assign to Variable”), not by declaring an empty one first. If you’re hunting for a standalone create flow, that’s why you can’t find the one you expected.
- Trusting the model to carry the ID. The entire reason variables exist is that free-text memory is unreliable. Move IDs and flags through input mappings and filters; don’t hope the model remembers across a long conversation.
- Confusing “the model can see it” with “it’s enforced.” A value in the instructions influences; only an input binding or a filter guarantees. Security-sensitive gating must be a filter.
A note on the moving target: Summer ‘26 continued to reshape this area — the ability to set context variables directly in the agent preview for more accurate testing, the ongoing rename of topics to subagents, and the lower-level Agent Script authoring layer all touch how variables are scoped and discussed. Verify names and behaviors against the exact release you’re on, and test the variable flows explicitly — a dropped variable is invisible in a single-turn test and glaring in a full conversation.
What to actually do
Design your variables before you design your topics. For every action, ask two questions: what does it need to know that an earlier step already established? — that’s a custom variable and an input mapping — and what condition must hold before it’s allowed to run? — that’s a filter over a variable. Use context variables for what the platform already knows about the session, custom variables for what you decide to remember, and reserve instruction-embedded variables for tone and behavior rather than for anything that has to be exactly right. Treat every pre-chat and API-supplied value as untrusted until the server validates it, and keep secrets out of anything the model can read. Do that, and the agent stops interrogating people for details it already has — which is most of the distance between a demo that impresses and an assistant customers actually trust.
Understanding the basics
What’s the difference between context, session, and custom variables in Agentforce?
Context variables are system-provided, prefixed $Context, and read-only by default — they hold session and user information mapped from records like MessagingSession. Custom variables (called conversation variables in metadata) are developer-defined, read/write, and persist across turns; they carry a value from one action’s output into another action’s input. “Session Variables” isn’t a separate type — it’s the section of the Agent Builder Context tab where you expose MessagingSession fields as context variables. In short: context variables are what the platform knows and hands you read-only; custom variables are what you decide to remember and pass along.
How do you pass a value from one Agentforce action to another?
Create a custom variable and use it as the bridge. On the first action’s output, choose “Assign to Variable” and map the output (for example, a verified customer ID) into a custom variable. On the later action’s input, assign that same variable. The value now flows deterministically between the two actions, so the model never has to re-ask the customer or reason it out of the transcript. Pair it with a filter — a condition over a variable that must be true before an action runs — when the passing of state also needs to gate whether the action is allowed to execute at all.
Are Agentforce pre-chat variables secure?
Not on their own. Hidden pre-chat fields are set client-side and can be altered by anyone who inspects the page, so treating a raw account ID or identity value from pre-chat as trusted is an impersonation risk. The safe pattern is to pass an encrypted, signed token and validate it server-side — have the Omni-Channel flow call Apex that decrypts, verifies the signature, and re-checks the identity before writing a verified value onto the session. Only that server-validated value should gate sensitive actions, and secrets should never live in a variable the model can read.
Building an agent that keeps losing the thread — re-asking for IDs, booking for the wrong person, leaking context it shouldn’t? Talk to us. Getting the variable and context architecture right so the agent behaves like one session is exactly the work we do.