Agent Script language reference — blocks, access and default_agent_user, variables and linked sources, subagents, reasoning and tools, before and after reasoning, utils, conditions, templates, operators, and where the docs and the compiler disagree
The Agent Script language that Agentforce agents are written in (.agent files in an aiAuthoringBundle), checked against Salesforce's open-source linter and compiler — required blocks, the access block that now holds default_agent_user, variables and the three linked sources, subagents, tools, before and after reasoning, the four @utils, if/else (no nested if), templates, operators, and where Salesforce's own docs, recipes and CLI templates disagree.
markdownreferencev1AV61-ZZD7stage: buildtrust: officialservice: agentforceinterface: agent-scriptAgent Script.agent fileAiAuthoringBundleconfig blockdeveloper_nameagent_typeAgentforceServiceAgentAgentforceEmployeeAgentaccess blockdefault_agent_user deprecated in configvariables blockmutablelinkedsource @MessagingSession.Id@MessagingEndUser.ContactId@VoiceCall.Idsystem blockconnection messagingOmniChannelFlowlanguage blockmodel_configstart_agent agent_routersubagenttopic keyword deprecatedreasoning instructions -> and |reasoning actions toolsbefore_reasoningafter_reasoning@utils.transition to@utils.escalate@utils.setVariables@utils.end_sessionsubagent as a tool returnsavailable whenwith ...set @outputsrun @actionsif elseelse ifnested if unsupportedtemplates {!}operators no multiplynaming rules
Agent Script
An agent is one .agent file inside an authoring bundle: aiAuthoringBundles/<Name>/<Name>.agent plus <Name>.bundle-meta.xml (bundleType AGENT). A complete, linted agent is in guides/agentforce/build-agent-with-agentforce-dx.md.
Check it offline: Salesforce publishes the parser, linter and compiler as open source (github.com/salesforce/agentscript). The hub builds it locally (work/tools/README-aslint.md). sf agent validate authoring-bundle compiles on Salesforce's servers and remains the final word.
Blocks
| Block | Required | Holds |
|---|---|---|
config | Yes | developer_name (letters, digits and single underscores; starts with a letter, no trailing underscore, 80 characters at most), agent_label, description, agent_type (AgentforceServiceAgent or AgentforceEmployeeAgent, among others), runtime and file_upload sub-blocks |
access | Service agents only (omit for employee agents) | default_agent_user (the agent user's username), sharing_policy, verified_customer_record_access |
variables | No | State (below) |
system | Yes | instructions, plus messages.welcome and messages.error |
connection messaging | For @utils.escalate | escalation_message, outbound_route_type: "OmniChannelFlow", outbound_route_name (an Omni-Channel flow in the org), adaptive_response_allowed |
language | No (required for voice) | default_locale, additional_locales |
model_config | No | model: "model://sfdc_ai__Default…"; also allowed per subagent |
start_agent <name> | Yes ("No start_agent block found" otherwise) | The router: description and reasoning with transition tools |
subagent <name> | At least one | description, optional label, system, actions, before_reasoning, reasoning, after_reasoning |
connected_subagent <name> | No (beta) | Another agent as a tool: target: "agentforce://<id>_<Name>" |
default_agent_user goes in access. In config it still works but the linter warns: "'default_agent_user' is deprecated: Property default_agent_user has moved from config to access. Move field to access block." A service agent without it fails: "'default_agent_user' is required for AgentforceServiceAgent type agents." Create the user with sf org create agent-user (reference/access/users-permissions-and-agent-users.md).
Keep the recipes' block order (config, variables, system, connection, knowledge, language, start_agent, subagents); the linter accepts others.
Variables
variables:
account_id: mutable string = ""
description: "Salesforce ID of the account under review"
approved: mutable boolean = False
session_id: linked string
description: "Messaging session ID"
source: @MessagingSession.Id
mutable: the agent reads and writes it. Give it a default. Types:string,number,boolean,object,date,timestamp,currency,id,list[T](no nested lists).linked: read-only, filled from the session. It needs asourceand can't have a default, an object type or a list type. The only sources the linter accepts are@MessagingSession(Id,MessagingEndUserId,EndUserLanguage),@MessagingEndUser(ContactId) and@VoiceCall(Id).visibility: "External"lets an API (the Agent API, family 5) set a variable; the default isInternal.- Values:
True,False,None, quoted strings and numbers.truefails: "'true' is not a valid value in AgentScript. Did you mean 'True' (capitalized)?" - System variables:
@system_variables.user_input(the latest utterance),.current_modality("voice"or"text"),.current_connection.
Subagents, reasoning and tools
subagent review_duplicates:
description: "Find duplicate candidates for an account"
reasoning:
instructions: ->
| Ask for the account if you don't have it.
if @variables.match_count > 0:
| {!@variables.match_count} candidates were found.
else:
| No candidates yet.
actions:
capture_account: @utils.setVariables
description: "Record the account ID the steward gives"
with account_id = ...
after_reasoning:
if @variables.approved == True:
transition to @subagent.confirm_merge
- Instructions are
->(a procedure:|lines,if,run,set) or|(a plain multi-line template). A quoted string is only allowed insystem. In a subagent it fails: "Expected procedure (->) or template (|) syntax". reasoning.actionsare tools the LLM may call. Each wraps a subagent action (@actions.x), a utility (@utils.x) or a subagent (@subagent.x). The subagent-levelactions:block declares what exists (reference/agentforce/agent-script-actions-and-targets.md).- On a tool:
with param = ...lets the LLM fill a value,with param = @variables.xor a literal binds it,set @variables.y = @outputs.zsaves an output, andavailable when <condition>hides the tool until the condition holds. before_reasoning/after_reasoningrun logic with no LLM:if,set,run,transition to, but no|lines.after_reasoningruns after the reasoning loop on every request. It's skipped if the subagent transitioned away. Neither is allowed in a subagent that uses the EinsteinHyperClassifier model.- Names in instruction text: write
{!@actions.find_duplicates}, not the bare name.
The four utilities
| Tool | Does |
|---|---|
@utils.transition to @subagent.x | One-way hand-off. In after_reasoning and other directives write a bare transition to @subagent.x. In reasoning.actions a bare transition fails: "'transition' is not a defined value". |
@subagent.x used as a tool | Runs that subagent, then returns to the caller |
@utils.setVariables | The LLM sets the variables named in with v = ... |
@utils.escalate | Hands off to a person through connection messaging. The linter doesn't require the block, but without it escalation has nowhere to go |
@utils.end_session | Ends the conversation |
Conditions, templates and operators
if/elseat the top level of a procedure. A nestediffails: "AgentScript does not support nested 'if' statements. Combine conditions with 'and'/'or', or restructure the logic."elifis a syntax error.else ifcompiles in the current open-source compiler, but Salesforce's docs say it isn't supported, so preferand/or.- Templates:
{!@variables.x}, with a conditional value{!@variables.s if @variables.s else "pending"}. - Operators:
==,!=,<,<=,>,>=,is,is not,and,or,not,+,-,.and[]. Multiplication fails: "Operator '*' is not supported".
Where Salesforce's sources disagree (October 2026)
| Topic | What the hub found |
|---|---|
default_agent_user | Docs and linter: the access block. The CLI's agent template (sf project generate --template agent), the CLI's authoring-bundle boilerplate and the recipes' rules file still write it in config, which triggers the deprecation warning |
| Linked source for the session ID | The recipes' rules file shows source: @session.sessionID, and the ContextHandling recipe uses @messagingSession.*. Both fail: "'source' must reference one of: @MessagingSession, @MessagingEndUser, @VoiceCall" |
| Conditions | Spec and docs say no else if, and the spec suggests nested if instead. The linter does the reverse: it allows else if and rejects nested if |
topic | Renamed subagent in April 2026. topic still parses, with a deprecation warning. Older blogs and the classic metadata (GenAiPlugin) still say topic |
| Voice | Docs list flat modality voice fields (voice_id, outbound_speed and so on). The newest recipe uses nested language, outbound.persona_id and outbound.model.id. Let Agent Builder's Voice Settings write the block |
| Rules the linter doesn't enforce | A mutable variable without a default, a missing system.messages, set on a utility tool, @inputs in set, and tabs all pass the linter. The recipes' guide says they fail on the server (the @inputs case "causes unknown error at deploy time"). Follow the guide |