Agent Script is most useful when you stop treating it as a more technical place to write prompts.
Its value is control over execution: explicit state, deterministic actions, conditional availability, guaranteed sequencing, and one-way transitions. Natural-language reasoning still matters, but it operates inside boundaries that can be reviewed, versioned, tested, and observed.
This guide builds a production-oriented service workflow and focuses on the details that determine whether Agent Script remains maintainable after the demo.
The mental model: compile the prompt, do not narrate the workflow
Agent Script contains two different kinds of instruction:
-> introduces logic that Agentforce resolves deterministically.
| introduces prompt material that is passed to the reasoning engine.
They can coexist in one subagent, but they do not execute at the same time. Logic instructions are processed top to bottom. Actions can run, outputs can be stored, conditions can be evaluated, and transitions can occur before the resulting prompt reaches the LLM.
reasoning:
instructions:
-> run @actions.get_order
with order_id = @variables.order_id
set @variables.order_status = @outputs.status
| Explain order {!@variables.order_id}.
| Use the verified status {!@variables.order_status}
reasoning:
instructions:
-> run @actions.get_order
with order_id = @variables.order_id
set @variables.order_status = @outputs.status
| Explain order {!@variables.order_id}.
| Use the verified status {!@variables.order_status}
reasoning:
instructions:
-> run @actions.get_order
with order_id = @variables.order_id
set @variables.order_status = @outputs.status
| Explain order {!@variables.order_id}.
| Use the verified status {!@variables.order_status}
This does not ask the model to remember to retrieve an order. The retrieval happens first. The model receives the resolved facts and the communication task.
Start with a state model
Before writing subagents, identify the minimum state that changes what the agent is allowed or required to do.
variables:
customer_id: mutable string = ""
verified: mutable boolean = False
order_id: mutable string = ""
order_status: mutable string = ""
return_eligible: mutable boolean = False
approval_required: mutable boolean = False
request_id: mutable string = ""
workflow_step: mutable string = "ROUTE"
variables:
customer_id: mutable string = ""
verified: mutable boolean = False
order_id: mutable string = ""
order_status: mutable string = ""
return_eligible: mutable boolean = False
approval_required: mutable boolean = False
request_id: mutable string = ""
workflow_step: mutable string = "ROUTE"
variables:
customer_id: mutable string = ""
verified: mutable boolean = False
order_id: mutable string = ""
order_status: mutable string = ""
return_eligible: mutable boolean = False
approval_required: mutable boolean = False
request_id: mutable string = ""
workflow_step: mutable string = "ROUTE"
Defaults are not cosmetic. A boolean with a known default supports reliable conditions. An unset value and an empty string are different states, and Agent Script supports is None when that distinction matters.
Do not create a variable for every noun in the conversation. Store a value when it will be reused, tested in a condition, passed to another action, shown later, or used to control availability. The transcript already contains conversational detail; variables are for operational state.
Make the agent router enforce global prerequisites
Every user utterance begins at the start_agent block. That makes it the right place for prerequisites that apply across domains.
start_agent agent_router:
description: "Routes verified customers to order and return support."
reasoning:
instructions:
-> if @variables.verified == False:
transition to @subagent.identity_verification
| Select the subagent that best matches the customer’s current request.
actions:
go_to_order_support: @utils.transition to @subagent.order_support
go_to_returns: @utils.transition to @subagent.returns
available when @variables.order_id != ""start_agent agent_router:
description: "Routes verified customers to order and return support."
reasoning:
instructions:
-> if @variables.verified == False:
transition to @subagent.identity_verification
| Select the subagent that best matches the customer’s current request.
actions:
go_to_order_support: @utils.transition to @subagent.order_support
go_to_returns: @utils.transition to @subagent.returns
available when @variables.order_id != ""start_agent agent_router:
description: "Routes verified customers to order and return support."
reasoning:
instructions:
-> if @variables.verified == False:
transition to @subagent.identity_verification
| Select the subagent that best matches the customer’s current request.
actions:
go_to_order_support: @utils.transition to @subagent.order_support
go_to_returns: @utils.transition to @subagent.returns
available when @variables.order_id != ""The first condition is stronger than hiding sensitive tools. It guarantees that an unverified user transitions before ordinary classification and before a prompt is sent to the LLM.
available when solves a different problem. It removes an option from the model’s tool set. Use it to reduce irrelevant choices or prevent premature actions. Do not rely on it alone when a workflow must execute.
Design identity verification as a state transition
The verification subagent should own one responsibility: establish a verified customer identity or escalate.
subagent identity_verification:
description: "Verifies a customer before protected account or order access."
reasoning:
instructions:
| Collect the customer’s email and verification code.
| Never claim verification succeeded unless {!@variables.verified} is True.
actions:
verify_customer: @actions.verify_customer_identity
with email = ...
with verification_code = ...
set @variables.customer_id = @outputs.customer_id
set @variables.verified = @outputs.verified
continue_after_verification: @utils.transition to @subagent.order_support
available when @variables.verified == True
escalate_verification: @utils.escalate
available when @variables.verified == Falsesubagent identity_verification:
description: "Verifies a customer before protected account or order access."
reasoning:
instructions:
| Collect the customer’s email and verification code.
| Never claim verification succeeded unless {!@variables.verified} is True.
actions:
verify_customer: @actions.verify_customer_identity
with email = ...
with verification_code = ...
set @variables.customer_id = @outputs.customer_id
set @variables.verified = @outputs.verified
continue_after_verification: @utils.transition to @subagent.order_support
available when @variables.verified == True
escalate_verification: @utils.escalate
available when @variables.verified == Falsesubagent identity_verification:
description: "Verifies a customer before protected account or order access."
reasoning:
instructions:
| Collect the customer’s email and verification code.
| Never claim verification succeeded unless {!@variables.verified} is True.
actions:
verify_customer: @actions.verify_customer_identity
with email = ...
with verification_code = ...
set @variables.customer_id = @outputs.customer_id
set @variables.verified = @outputs.verified
continue_after_verification: @utils.transition to @subagent.order_support
available when @variables.verified == True
escalate_verification: @utils.escalate
available when @variables.verified == FalseThe slot-fill token ... tells the reasoning engine to supply an input. Use it for values that genuinely require conversational collection. Do not use slot filling for chained deterministic actions, because those inputs must come from known variables or prior outputs.
Fetch before reasoning when the data is mandatory
If every response in a subagent needs the same record, retrieve it in logic instructions rather than exposing retrieval as an optional tool.
subagent order_support:
description: "Answers status and delivery questions for a known customer order."
reasoning:
instructions:
-> if @variables.order_id != "":
run @actions.get_order
with customer_id = @variables.customer_id
with order_id = @variables.order_id
set @variables.order_status = @outputs.status
| Answer only from the retrieved order result.
| If the order identifier is missing, ask for it and use {!@actions.capture_order_id}.
actions:
capture_order_id: @utils.setVariables
with order_id = ...
go_to_returns: @utils.transition to @subagent.returns
available when @variables.order_status == "DELIVERED"subagent order_support:
description: "Answers status and delivery questions for a known customer order."
reasoning:
instructions:
-> if @variables.order_id != "":
run @actions.get_order
with customer_id = @variables.customer_id
with order_id = @variables.order_id
set @variables.order_status = @outputs.status
| Answer only from the retrieved order result.
| If the order identifier is missing, ask for it and use {!@actions.capture_order_id}.
actions:
capture_order_id: @utils.setVariables
with order_id = ...
go_to_returns: @utils.transition to @subagent.returns
available when @variables.order_status == "DELIVERED"subagent order_support:
description: "Answers status and delivery questions for a known customer order."
reasoning:
instructions:
-> if @variables.order_id != "":
run @actions.get_order
with customer_id = @variables.customer_id
with order_id = @variables.order_id
set @variables.order_status = @outputs.status
| Answer only from the retrieved order result.
| If the order identifier is missing, ask for it and use {!@actions.capture_order_id}.
actions:
capture_order_id: @utils.setVariables
with order_id = ...
go_to_returns: @utils.transition to @subagent.returns
available when @variables.order_status == "DELIVERED"This pattern reduces two failure modes: the model answering from stale conversational memory and the model deciding retrieval is unnecessary. It also makes telemetry easier because the data dependency is visible in the script.
Chain actions around typed outcomes
Action chaining is where deterministic workflows become operational. Suppose a return request must evaluate eligibility, create a case, and write an audit event in order.
subagent returns:
description: "Evaluates and creates returns for delivered orders."
reasoning:
instructions:
| Explain the return outcome using verified action outputs.
| Never promise a refund before a return request exists.
actions:
create_return: @actions.evaluate_return_eligibility
with customer_id = @variables.customer_id
with order_id = @variables.order_id
set @variables.return_eligible = @outputs.eligible
set @variables.approval_required = @outputs.approval_required
run @actions.create_return_request
with order_id = @variables.order_id
with eligible = @variables.return_eligible
set @variables.request_id = @outputs.request_id
run @actions.write_agent_audit_event
with customer_id = @variables.customer_id
with request_id = @variables.request_id
with event_type = "RETURN_CREATED"subagent returns:
description: "Evaluates and creates returns for delivered orders."
reasoning:
instructions:
| Explain the return outcome using verified action outputs.
| Never promise a refund before a return request exists.
actions:
create_return: @actions.evaluate_return_eligibility
with customer_id = @variables.customer_id
with order_id = @variables.order_id
set @variables.return_eligible = @outputs.eligible
set @variables.approval_required = @outputs.approval_required
run @actions.create_return_request
with order_id = @variables.order_id
with eligible = @variables.return_eligible
set @variables.request_id = @outputs.request_id
run @actions.write_agent_audit_event
with customer_id = @variables.customer_id
with request_id = @variables.request_id
with event_type = "RETURN_CREATED"subagent returns:
description: "Evaluates and creates returns for delivered orders."
reasoning:
instructions:
| Explain the return outcome using verified action outputs.
| Never promise a refund before a return request exists.
actions:
create_return: @actions.evaluate_return_eligibility
with customer_id = @variables.customer_id
with order_id = @variables.order_id
set @variables.return_eligible = @outputs.eligible
set @variables.approval_required = @outputs.approval_required
run @actions.create_return_request
with order_id = @variables.order_id
with eligible = @variables.return_eligible
set @variables.request_id = @outputs.request_id
run @actions.write_agent_audit_event
with customer_id = @variables.customer_id
with request_id = @variables.request_id
with event_type = "RETURN_CREATED"The action contracts should return machine-meaningful fields. Prefer eligible: false and reason_code: RETURN_WINDOW_EXPIRED over a paragraph. The script can branch on the former; the LLM can explain the latter.
Make write actions idempotent. If the reasoning engine, network, or user retries, the same idempotency key should return the existing request rather than creating a duplicate.
Separate deterministic branching from explanatory language
A strong hybrid pattern branches in logic and communicates in prompt instructions.
reasoning:
instructions:
-> if @variables.return_eligible == True:
| Confirm that return request {!@variables.request_id} was created.
-> if @variables.approval_required == True:
| Explain that approval is required before funds are issued.
-> else:
| Explain the standard refund timeline.
-> else:
| Explain that the order is not eligible.
| State the policy reason from the action output without inventing alternatives
reasoning:
instructions:
-> if @variables.return_eligible == True:
| Confirm that return request {!@variables.request_id} was created.
-> if @variables.approval_required == True:
| Explain that approval is required before funds are issued.
-> else:
| Explain the standard refund timeline.
-> else:
| Explain that the order is not eligible.
| State the policy reason from the action output without inventing alternatives
reasoning:
instructions:
-> if @variables.return_eligible == True:
| Confirm that return request {!@variables.request_id} was created.
-> if @variables.approval_required == True:
| Explain that approval is required before funds are issued.
-> else:
| Explain the standard refund timeline.
-> else:
| Explain that the order is not eligible.
| State the policy reason from the action output without inventing alternatives
Agent Script currently supports if and else, not an else if chain. For multiple mutually exclusive states, use nested conditions or simplify the action output into a small set of booleans and status codes.
Understand one-way transitions
A transition is not a function call. When Agentforce executes transition to, it stops the current directive block, discards the current resolved prompt, and processes the destination subagent. Control does not automatically return.
This has two important consequences:
Put mandatory transitions before expensive actions. Anything executed before the transition adds latency and may be discarded.
Design the destination subagent with all state it needs. Do not assume the previous prompt follows it.
If you want the LLM to choose a specialist and then resume the original task, expose the specialist as a reasoning tool rather than using an unconditional transition. The architectural choice is “move” versus “delegate,” not merely syntax.
Use tool availability to reduce the decision surface
Models become less reliable when several tools are semantically similar or technically impossible in the current state. available when makes the tool set contextual.
reasoning:
actions:
create_return: @actions.create_return
available when @variables.verified == True and @variables.order_status == "DELIVERED"
cancel_order: @actions.cancel_order
available when @variables.verified == True and @variables.order_status == "PENDING"
escalate_high_value: @utils.escalate
available when @variables.approval_required == Truereasoning:
actions:
create_return: @actions.create_return
available when @variables.verified == True and @variables.order_status == "DELIVERED"
cancel_order: @actions.cancel_order
available when @variables.verified == True and @variables.order_status == "PENDING"
escalate_high_value: @utils.escalate
available when @variables.approval_required == Truereasoning:
actions:
create_return: @actions.create_return
available when @variables.verified == True and @variables.order_status == "DELIVERED"
cancel_order: @actions.cancel_order
available when @variables.verified == True and @variables.order_status == "PENDING"
escalate_high_value: @utils.escalate
available when @variables.approval_required == TrueThis is more maintainable than telling the model “do not choose cancel_order unless…” because the invalid tool is absent. Still, use a deterministic transition or action-side validation when the rule is a security or compliance requirement. Defense in depth matters: script controls the reasoning surface; the action enforces the transaction.
Failure handling belongs in the contract
An enterprise action should distinguish at least four outcomes:
SUCCESS: the mutation committed and an identifier exists.
BUSINESS_REJECTED: the request is valid but policy disallows it.
RETRYABLE_ERROR: no mutation committed and retry is safe.
TERMINAL_ERROR: the agent must stop or escalate.
Return these as typed fields. Never ask the LLM to infer whether a timeout occurred before or after commit. That is the action’s responsibility.
{
"status": "RETRYABLE_ERROR",
"committed": false,
"safe_to_retry": true,
"correlation_id": "af-8f1c2",
"customer_message_code": "SERVICE_TEMPORARILY_UNAVAILABLE"
}{
"status": "RETRYABLE_ERROR",
"committed": false,
"safe_to_retry": true,
"correlation_id": "af-8f1c2",
"customer_message_code": "SERVICE_TEMPORARILY_UNAVAILABLE"
}{
"status": "RETRYABLE_ERROR",
"committed": false,
"safe_to_retry": true,
"correlation_id": "af-8f1c2",
"customer_message_code": "SERVICE_TEMPORARILY_UNAVAILABLE"
}Version control and deployment discipline
Agent Script is whitespace-sensitive and compiles to lower-level metadata. Treat it like source code:
Retrieve it through Agentforce DX into the Salesforce DX project.
Review script diffs alongside Apex, Flow, permissions, and action schema changes.
Run validation before deployment and block merges on syntax errors.
Promote the script and its referenced resources together.
Keep environment-specific identifiers out of narrative instructions where possible.
The most dangerous deployment is a valid script pointing at an action whose input or output contract changed independently.
A testing strategy that catches control failures
Test the script as a state machine, not only as a chatbot.
1. Branch coverage
Create fixtures for every condition: verified and unverified, order present and missing, eligible and ineligible, approval required and not required.
2. Invariant tests
No protected action is available when verified == False.
No success response appears unless request_id is populated.
Every committed write produces an audit event.
Every approval-required outcome routes to a human or approval workflow.
3. Mutation tests
Deliberately invert a condition, remove a transition, or return a malformed action status. A useful suite should fail. If it remains green, it is testing conversation style rather than control.
4. Replay tests
Run the same production-like sessions after changes to script, models, actions, and grounding. Compare actions selected, transitions taken, variable state, latency, and final response—not just response text.
Observability: log the path, not only the answer
For each session, capture the selected subagent, tools made available, actions executed, normalized outcomes, variable changes, transitions, retries, and final disposition. This creates evidence for debugging and audit.
A response can be linguistically excellent while the execution path is wrong. Path-level telemetry lets you see that the agent reached the right sentence through an invalid sequence before that sequence causes a visible incident.
The production rule
Put business meaning in action contracts, workflow state in variables, mandatory order in logic instructions, optional judgment in the reasoning engine, and customer-facing language in prompt instructions.
Agent Script is not valuable because it makes an agent deterministic. It is valuable because it lets you decide exactly which parts must be deterministic—and prove that they were.
Sources and further reading