---
cuppy_hub: salesforce
cuppy_path: reference/agentforce/agent-script.md
cuppy_section: docs
cuppy_doc: AV61-ZZD7
cuppy_doc_uuid: a0407d59-cc9c-47c8-bc7e-bf9153679afa
cuppy_permalink: https://cuppy.io/salesforce/d/AV61-ZZD7
cuppy_version: 1
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/salesforce/docs/reference/agentforce/agent-script
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: 7ff57d499a0329e22bd190b250faee784b23d3197938c2e3cc03d8b93a8b2c75
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# 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

```agentscript
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 a `source` and 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 is `Internal`.
- Values: `True`, `False`, `None`, quoted strings and numbers. `true` fails: "'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

```agentscript
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 in `system`. In a subagent it fails: "Expected procedure (->) or template (|) syntax".
- **`reasoning.actions` are tools** the LLM may call. Each wraps a subagent action (`@actions.x`), a utility (`@utils.x`) or a subagent (`@subagent.x`). The subagent-level `actions:` 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.x` or a literal binds it, `set @variables.y = @outputs.z` saves an output, and `available when <condition>` hides the tool until the condition holds.
- **`before_reasoning` / `after_reasoning`** run logic with no LLM: `if`, `set`, `run`, `transition to`, but no `|` lines. `after_reasoning` runs 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` / `else` at the top level of a procedure. **A nested `if` fails**: "AgentScript does not support nested 'if' statements. Combine conditions with 'and'/'or', or restructure the logic."
- `elif` is a syntax error. `else if` compiles in the current open-source compiler, but Salesforce's docs say it isn't supported, so prefer `and`/`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 |


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 376 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/salesforce/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
