/

Agent Script in Production: State, Gates, Chains, and Proof

Engineering

18 min read

Agent Script in Production: State, Gates, Chains, and Proof

Agent Script in Production: State, Gates, Chains, and Proof

Agent Script in Production: State, Gates, Chains, and Proof

A hands-on engineering guide to deterministic Agentforce workflows with explicit state, action contracts, routing gates, and testable execution paths.

A hands-on engineering guide to deterministic Agentforce workflows with explicit state, action contracts, routing gates, and testable execution paths.

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 == False
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 == False
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 == False

The 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 == True
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 == True
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 == True

This 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