21 KiB
Instruction Resolution
How Agent Script instructions are processed at runtime: from static text to dynamic LLM prompts.
1. Runtime Lifecycle
AgentScript defines deterministic lifecycle hooks around an iterative LLM reasoning loop:
before_reasoning (once per turn)
-> resolve reasoning.instructions (every reasoning iteration)
-> LLM calls a tool or produces a user response
-> tool call: execute it, then resolve reasoning.instructions again
-> response: end the loop
-> after_reasoning (once, if execution reaches the hook)
Do not confuse a new reasoning iteration after a tool call with
after_reasoning. The latter does not run after each tool.
2. Instruction Surfaces
| Surface | Runtime meaning | Authoring use |
|---|---|---|
Global system.instructions |
Default system prompt for every execution block | Durable identity, safety, scope, and response invariants |
Subagent system.instructions |
Replaces the global system prompt for that subagent; it does not merge | Rare specialist override that restates every invariant still required |
reasoning.instructions |
Runtime-resolved instructions rebuilt on every reasoning iteration | Current objective, relevant state, action guidance, and stop conditions |
before_reasoning |
Deterministic procedure that runs once before the reasoning loop | Preconditions, data preparation, and early transitions |
after_reasoning |
Deterministic procedure that runs if execution reaches the hook after the reasoning loop | Final state updates and post-response transitions |
Omit a subagent system override when the global instructions already apply. If an override is necessary, copy the durable invariants that subagent must retain; the compiler selects the subagent value instead of the global value.
Treat effective system and reasoning instructions as cumulative. Do not depend on a model provider separating or prioritizing the AgentScript surfaces differently unless the target runtime's public, versioned contract guarantees that behavior. Their combined meaning must contain no contradiction, duplicate policy, or reliance on one layer overriding the other.
Separate AgentScript Control from Model Instructions
Subagents and variables are AgentScript runtime concepts, not model knowledge. The compiler and runtime select the current execution block, evaluate conditions, run deterministic actions, update state, interpolate values, and expose the currently available tools. Portable model instructions must not depend on a provider exposing structured execution-block identity or direct variable-store access unless the target runtime's public contract guarantees it.
Developer documentation may explain subagents and variables because authors
need that mental model. Text sent to the model must instead state the concrete
task and response duty. Do not tell the model to inspect the active subagent or
read @variables.
Scope Response Duties by Branch
Do not put an unconditional response duty in the global layer when any branch must route, verify, clarify, refuse, close, or escalate without answering. This conflict is especially common in router-first agents:
# WRONG: the effective prompt says both answer and do not answer.
system:
instructions: "Answer the user's questions helpfully."
start_agent agent_router:
reasoning:
instructions: ->
| Do not answer. Route the request.
# RIGHT: the global rule allows the current operating task to set posture.
system:
instructions: |
Perform only the current operating task described below. Answer the
underlying request only when that task calls for an answer. Otherwise
route, verify, clarify, refuse, or escalate as directed.
Review the effective pair for every execution branch: global or replacement system instructions plus the branch's resolved reasoning instructions. A scope narrowing is compatible only when the global layer already permits that narrowing. For every reachable LLM call, that effective pair must prescribe one unambiguous response posture. A subagent system replacement must restate every durable invariant it retains.
Deterministic Resolution Before Each LLM Call
Before every reasoning iteration, the runtime evaluates deterministic
constructs in reasoning.instructions: ->. This happens before the LLM sees
the resolved reasoning instructions.
What Happens During Resolution
if/elseevaluation: Conditions are evaluated against current variable values. Only the matching branch is included in the prompt.- Variable injection:
{!@variables.X}tokens are replaced with current values. runexecution: Deterministicrun @actions.Xcalls execute and their outputs are captured.setexecution: Variable assignments execute immediately.transition to: If reached, the subagent switch happens immediately (LLM is never called).
Resolution Example
Given this instruction block:
reasoning:
instructions: ->
# Completed-state check from a previous tool call
if @variables.order_status != "":
| Report order status {!@variables.order_status}, then stop.
transition to @subagent.confirmation
| Collect the order number, then use {!@actions.lookup_order} once.
Before lookup, the LLM sees only:
Collect the order number, then use lookup_order once.
After the tool stores order_status, the instructions are rebuilt. The
completed-state branch resolves and transitions without asking the LLM to call
the tool again.
3. LLM Processing
AgentScript guarantees the authored semantics, not an exact provider message count. Reasoning receives:
- One authored node-instruction value: the subagent system override when
present, otherwise the global
system.instructions. Runtime base prompts and metadata may add other system content outside AgentScript. - Conversation history.
- The resolved
reasoning.instructions, when non-empty. - The currently available tools and their schemas.
Do not document or test an exact four-message structure. Tool schemas are not necessarily encoded as a chat message, and message transport is not the AgentScript authoring contract.
What the LLM Decides
Based on the assembled prompt, the LLM:
- Selects an action (if applicable) from the available actions list
- Fills slot parameters (
...values) from conversation context - Generates a text response to send to the user
- Decides whether to transition (if a transition action is available and appropriate)
What the LLM Does NOT See
- Raw
if/elseblocks (already resolved before the iteration) - Structured AgentScript execution-block identity or direct variable-store access unless the target runtime explicitly provides it
runstatements (already executed before the iteration)setstatements (already executed)available whenconditions (already evaluated -- hidden actions are simply absent)after_reasoningblocks (run after the LLM, not shown to it)
4. Tool Loop and Re-Resolution
After the LLM selects and executes a tool, the runtime begins another reasoning
iteration and rebuilds reasoning.instructions from current state.
Loop Sequence
1. Resolve reasoning instructions
2. LLM reasons and selects a tool
3. Action executes -> outputs captured in variables
4. Re-resolve reasoning instructions with updated variables
- Post-action checks at TOP of instructions fire
- New data is injected into the prompt
5. LLM reasons again with updated context
6. Repeat until a transition occurs or the LLM produces a user response
Only after the LLM produces a user response does the reasoning loop end and
after_reasoning run.
Why Post-Action Checks Go at the TOP
Place post-action checks at the TOP of instructions: -> so they fire immediately on re-resolution:
reasoning:
instructions: ->
# POST-ACTION CHECK (at TOP -- fires on re-resolution)
if @variables.order_cancelled == True:
| Your order has been cancelled successfully.
transition to @subagent.confirmation
# These instructions are for the FIRST entry (before action runs)
| I can help you cancel your order.
| What is your order number?
If the check were at the BOTTOM, the LLM would see the "ask for order number" instructions again even after the cancellation succeeded, causing confusion.
5. Concise Reasoning Instructions
Keep reasoning.instructions task-local. Include only information that can
change the current tool choice or response:
- Checks backed by trusted outcomes or material invariants.
- Deterministic data loading required for this iteration.
- The current objective and only the resolved state it consumes.
- Action guidance, exclusions, and a stop condition.
Do not add a variable or branch merely to remind the model what happened in surviving conversation history.
Keep persona, tone, disclosure, safety, and broad scope rules in the effective system layer. Put tool-specific trigger details in action descriptions instead of repeating them in the reasoning instructions.
reasoning:
instructions: ->
# Trusted verification output controls the protected capability.
if @variables.is_verified == True:
| Complete the requested account task using the available action once.
if @variables.is_verified == False:
| Ask for the minimum information needed to verify identity. Do not use account-changing actions.
6. Common Instruction Patterns
Pattern 1: Security Gate
Prevent access to sensitive actions until identity is verified:
reasoning:
instructions: ->
if @variables.is_verified == False:
| You must verify your identity before I can help with account changes.
| Please provide your email address.
if @variables.is_verified == True:
| Identity verified. I can now help with account changes.
| What would you like to do?
actions:
update_account: @actions.update_account_info
description: "Update account information"
available when @variables.is_verified == True
with field = ...
with value = ...
The available when guard hides the action from the LLM until verification passes. The conditional instructions tell the user what to do.
Pattern 2: Data-Dependent Instructions
Load data first, then tailor instructions based on the result:
reasoning:
instructions: ->
run @actions.get_account_status
with account_id = @variables.account_id
set @variables.account_status = @outputs.status
set @variables.balance = @outputs.balance
| Account status: {!@variables.account_status}
| Current balance: {!@variables.balance}
if @variables.account_status == "delinquent":
| IMPORTANT: This account is delinquent.
| Collect payment before processing any other requests.
| Offer payment plan options if customer cannot pay in full.
if @variables.account_status == "active":
| This account is in good standing.
| Process requests normally.
Pattern 3: Action Chaining
Execute one action, then use its output to drive the next:
reasoning:
instructions: ->
# Post-action check: case was created in previous loop
if @variables.case_id != "":
| Case {!@variables.case_id} has been created.
run @actions.assign_case
with case_id = @variables.case_id
with priority = @variables.priority
transition to @subagent.case_confirmation
| I need to collect some information to create a support case.
| What is the issue you're experiencing?
Pattern 4: Machine-Known Gate
Use compound conditions when trusted machine state controls a material capability. Do not persist the user's current intent merely to route it; let the router infer current intent from the latest turn and conversation history.
reasoning:
instructions: ->
if @variables.is_verified == True and @variables.account_locked == False:
| Complete the requested account task using the available action.
if @variables.is_verified == False:
| Ask for the minimum information needed to verify identity.
if @variables.account_locked == True:
| Do not use account-changing actions. Explain how to unlock the
account or escalate.
7. Anti-Patterns to Avoid
Anti-Pattern 1: Re-Explaining Language Syntax
Keep this reference focused on instruction resolution. For supported
if / else if / else, invalid elif, nested-condition limitations, and
post-action conditionals, use the canonical
Conditional Control Flow Syntax
section.
Anti-Pattern 2: Post-Action Check at Bottom
# WRONG -- Check at bottom; LLM sees stale instructions on re-resolution
reasoning:
instructions: ->
| What is your order number?
if @variables.order_status != "":
transition to @subagent.show_status
# CORRECT -- Check at TOP
reasoning:
instructions: ->
if @variables.order_status != "":
transition to @subagent.show_status
| What is your order number?
Anti-Pattern 3: Persona in Subagent Instructions
# WRONG -- Persona text duplicated in every subagent
reasoning:
instructions: |
You are a friendly, professional customer service agent.
Help the customer with their order.
# CORRECT -- Persona in system instructions, subagent has operational instructions only
system:
instructions: |
You are a friendly, professional customer service agent.
subagent order_support:
reasoning:
instructions: ->
| Help the customer check their order status.
| Ask for the order number if not provided.
Anti-Pattern 4: Using | When -> Is Needed
# WRONG -- Using literal mode when conditionals are needed
reasoning:
instructions: |
if @variables.is_verified == True:
Show account details.
# The above sends the literal text "if @variables.is_verified == True:" to the LLM!
# CORRECT -- Use procedural mode for conditionals
reasoning:
instructions: ->
if @variables.is_verified == True:
| Show account details.
Anti-Pattern 5: Missing Variable Injection Syntax
# WRONG -- Variable name as literal text
reasoning:
instructions: ->
| Your order ID is @variables.order_id
# CORRECT -- Use injection syntax
reasoning:
instructions: ->
| Your order ID is {!@variables.order_id}
Pattern: after_reasoning Lifecycle Actions
run is supported in after_reasoning through the common action mechanism.
The block runs after the reasoning loop ends, including a turn where the model
produced a response without calling a tool. Use it only when the follow-up must
run after every completed reasoning pass.
Do not use this lifecycle hook for an irreversible action merely because it is deterministic; consequential-action preconditions still need explicit guards.
# Log every completed reasoning turn.
after_reasoning:
run @actions.log_event
with event = "turn_completed"
Anti-Pattern 7: Prose-Based Conditional Logic
# WRONG -- Conditional behavior described in prose; the LLM must interpret
# these directives and may ignore, reorder, or misapply them
reasoning:
instructions: ->
| If the user is a VIP, offer priority support.
| If they haven't been verified, ask for verification first.
| If the refund has been approved, confirm it and end the conversation.
| Check availability before booking.
# CORRECT when the conditions are trusted machine facts protecting material
# invariants. The LLM sees only the matching operating instructions.
reasoning:
instructions: ->
if @variables.refund_approved == True:
| Your refund has been confirmed. Reference: {!@variables.refund_id}
transition to @subagent.confirmation
if @variables.customer_verified == False:
| I need to verify your identity before proceeding.
| Please provide your email address.
if @variables.customer_tier == "vip":
| As a VIP customer, you have access to priority support.
| How can I help you today?
Why this matters: machine-known authorization, confirmation, action-result, and external-ordering conditions must not depend on model discretion. AgentScript resolves those branches before the model sees the prompt.
Do not manufacture state merely so conversational judgment can become an
if/else. Use a deterministic branch only when a named runtime consumer and
material cause justify it. Reserve prose for current intent, tone, phrasing,
and other decisions the model should interpret from the conversation.
8. Syntax Patterns Reference
Literal Mode (|)
Static text passed directly to the LLM. No evaluation occurs:
instructions: |
Help the customer with their order.
Be professional and concise.
Or with the | prefix on each line (inside procedural mode):
instructions: ->
| Help the customer with their order.
| Be professional and concise.
Procedural Mode (->)
Enables conditionals, variable injection, and deterministic actions:
instructions: ->
if @variables.condition == True:
| Text shown when condition is true.
else:
| Text shown when condition is false.
Variable Injection
| Your order {!@variables.order_id} is {!@variables.status}.
Deterministic Run
run @actions.load_data
with param = @variables.value
set @variables.result = @outputs.field
Deterministic Set
set @variables.counter = @variables.counter + 1
Deterministic Transition
transition to @subagent.next_subagent
Conditional Transition
if @variables.all_collected == True:
transition to @subagent.confirmation
9. Programmatic Trace Access
To verify how instructions were resolved at runtime, use the trace files generated by sf agent preview.
Trace File Location
.sfdx/agents/{BundleName}/sessions/{sessionId}/traces/{planId}.json
Reading Instruction Resolution from Traces
# Extract the resolved instructions that the LLM received
jq -r '.planTrace.steps[] | select(.type == "LLM_STEP") | .input' \
~/.sf/sfdx/agents/MyAgent/sessions/*/traces/*.json
# Extract the LLM's response
jq -r '.planTrace.steps[] | select(.type == "LLM_STEP") | .output' \
~/.sf/sfdx/agents/MyAgent/sessions/*/traces/*.json
# Check which variables were set during resolution
jq -r '.planTrace.steps[] | select(.type == "ACTION_STEP") | {name: .name, pre: .preVars, post: .postVars}' \
~/.sf/sfdx/agents/MyAgent/sessions/*/traces/*.json
Verifying Per-Iteration Resolution
To confirm that if/else blocks resolved correctly, compare the trace's LLM_STEP input against your instructions: -> block. The LLM input should contain only the branches that matched, with all {!@variables.X} tokens replaced with actual values.
If the trace shows unexpected instruction text:
- Check that you used
->mode (not|mode) when conditionals are present - Verify variable values at the time of resolution (check
preVarson precedingACTION_STEP) - Confirm that
ifconditions use the correct comparison operators
Using STDM for Production Trace Analysis
For production agents, use the Session Trace Data Model (STDM) in Data Cloud to access trace data programmatically. The STDM captures LLM_STEP records with input and output fields that contain the resolved prompt and LLM response. This is useful for auditing instruction resolution at scale across hundreds of live sessions.
10. Resolution Across Subagent Transitions
When a subagent transition occurs (via @utils.transition to @subagent.X or transition to @subagent.X), instruction resolution starts fresh in the new subagent:
- The current subagent's remaining instructions are NOT processed
- The new subagent's
before_reasoning:runs (if present) - The new subagent's
reasoning: instructions:resolves for its first iteration - The LLM receives the new subagent's assembled prompt
Important: Variables persist across transitions. A variable set in Subagent A is available in Subagent B. This is how you pass data between subagents:
# Subagent A: Collect data
subagent collect_info:
reasoning:
instructions: ->
| Please provide your order number.
actions:
capture_order: @actions.get_order_id
with input = ...
set @variables.order_id = @outputs.order_id
after_reasoning:
if @variables.order_id != "":
transition to @subagent.process_order
# Subagent B: Use the data
subagent process_order:
reasoning:
instructions: ->
# order_id is available from Subagent A
| Processing order {!@variables.order_id}...
run @actions.get_order_details
with order_id = @variables.order_id
set @variables.order_status = @outputs.status