SF-AI-AgentScript Skill
"Prompt engineering is like writing laws in poetry - beautiful, but not enforceable."
Agent Script transforms agent development from prompt-based suggestions to code-enforced guarantees. This skill guides you through writing, debugging, testing, and deploying Agentforce agents using the Agent Script DSL.
⚠️ CRITICAL WARNINGS
API & Version Requirements
| Requirement | Value | Notes |
|---|---|---|
| API Version | 65.0+ | Required for Agent Script support |
| License | Agentforce | Required for agent authoring |
| Einstein Agent User | Required | Must exist in org for default_agent_user |
| File Extension | .agent |
Single file contains entire agent definition |
MANDATORY Pre-Deployment Checks
default_agent_userMUST be valid - Query:SELECT Username FROM User WHERE Profile.Name = 'Einstein Agent User' AND IsActive = true- No mixed tabs/spaces - Use consistent indentation (2-space, 3-space, or tabs - never mix)
- Booleans are capitalized - Use
True/False, nottrue/false - Exactly one
start_agentblock - Multiple entry points cause compilation failure
⛔ SYNTAX CONSTRAINTS (Validated via Testing + Official Spec)
| Constraint | ❌ WRONG | ✅ CORRECT |
|---|---|---|
No else if keyword; no nested if |
else if x: or else: + nested if (both invalid) |
if x and y: (compound), or flatten to sequential ifs |
No top-level actions: block |
actions: at root level |
Actions only inside topic.reasoning.actions: |
No inputs:/outputs: in action INVOCATIONS (Level 2) |
inputs: block inside reasoning.actions: invocation |
Use with/set in reasoning.actions: invocations. The topic-level actions: definitions DO use inputs:/outputs: blocks. |
Multiple available when supported |
(previously listed as error) | available when A + available when B on same action is valid (TDD validated 2026-02-14). Org-dependent: compiles on AgentforceTesting but REJECTED on some orgs with "Duplicate 'available when' clause." Use compound and conditions for portability. |
| Avoid reserved action names | escalate: @utils.escalate |
escalate_now: @utils.escalate |
... is slot-filling only |
my_var: mutable string = ... |
my_var: mutable string = "" |
| No defaults on linked vars | id: linked string = "" |
id: linked string + source: |
| Linked vars: no object/list | data: linked object |
Use linked string or parse in Flow |
| Post-action only on @actions | @utils.X with set/run |
Only @actions.X supports post-action |
| agent_name must match folder | Folder: MyAgent, config: my_agent |
Both must be identical (case-sensitive) |
| Reserved field names | description: string, label: string |
Use descriptions, label_text, or suffix with _field |
🔴 Reserved Field Names (Breaking in Recent Releases)
Common field names that cause parse errors when used as variable or I/O field names:
❌ RESERVED as variable/field names:
description, label, is_required, is_displayable, is_used_by_planner
✅ WORKAROUNDS for variable/field names:
description → descriptions, description_text, desc_field
label → label_text, display_label, label_field
Important distinction (TDD v2.2.0):
is_required,is_displayable,is_used_by_planner, andlabelare reserved as variable/field names but are valid as action I/O metadata properties. For example, you cannot name a variablelabel, but you CAN uselabel:as a property on an action definition, input, or output. See "Action I/O Metadata Properties" below.
🔴 Feature Validity by Context (TDD Validated v2.2.0)
Key distinction: Many action metadata properties are valid on action definitions with targets (
flow://,apex://) but NOT on utility actions (@utils.transition). The v1.3.0 finding that tested only@utils.transitionwas too narrow — v2.2.0 corrects this.
| Feature | On @utils.transition |
On action definitions with target: |
Notes |
|---|---|---|---|
label: on topics |
❌ v1.3.0 | ✅ v2.2.0 | Valid on topic blocks |
label: on actions |
❌ v1.3.0 | ✅ v2.2.0 | Valid on Level 1 action definitions |
label: on I/O fields |
❌ v1.3.0 | ✅ v2.2.0 | Valid on inputs/outputs |
require_user_confirmation: |
❌ | ✅ v2.2.0 | Compiles; runtime no-op (Issue 6) |
include_in_progress_indicator: |
❌ | ✅ v2.2.0 | Shows spinner during action execution |
progress_indicator_message: |
❌ | ✅ v2.2.0 | Works on both flow:// and apex:// |
output_instructions: |
❌ | ❓ Untested | Not tested on target-backed actions |
always_expect_input: |
❌ | ❌ | NOT implemented anywhere |
What works on @utils.transition actions:
actions:
go_next: @utils.transition to @topic.next
description: "Navigate to next topic" # ✅ ONLY description works
What works on action definitions with target::
actions:
process_order:
label: "Process Order" # ✅ Display label
description: "Process the customer's order" # ✅ LLM description
require_user_confirmation: True # ✅ Compiles (runtime Issue 6)
include_in_progress_indicator: True # ✅ Shows spinner
progress_indicator_message: "Processing..." # ✅ Custom spinner message
inputs:
order_id: string
label: "Order ID" # ✅ I/O display label
description: "The order identifier"
outputs:
status: string
label: "Order Status" # ✅ I/O display label
description: "Current order status"
target: "apex://OrderProcessor"
🔴 complex_data_type_name Mapping Table (Critical for Actions)
"#1 source of compile errors" - Use this table when defining action inputs/outputs in Agentforce Assets.
| Data Type | complex_data_type_name Value |
Notes |
|---|---|---|
string |
(none needed) | Primitive type |
number |
(none needed) | Primitive type |
boolean |
(none needed) | Primitive type |
object (SObject) |
lightning__recordInfoType |
Use for Account, Contact, etc. |
list[string] |
lightning__textType |
Collection of text values |
list[object] |
lightning__textType |
Serialized as JSON text |
| Apex Inner Class | @apexClassType/NamespacedClass__InnerClass |
Namespace required |
| Custom LWC Type | lightning__c__CustomTypeName |
Custom component types |
| Currency field | lightning__currencyType |
For monetary values |
datetime |
lightning__dateTimeStringType |
DateTime values (TDD v2.1.0) |
Agent Script → Lightning Type Mapping (TDD Validated v2.1.0):
Use this when troubleshooting type errors between Agent Script action I/O and Apex/Flow targets.
| Agent Script Type | Lightning Type | Apex Type | Notes |
|---|---|---|---|
string |
lightning__textType |
String |
No complex_data_type_name needed |
number |
lightning__numberType |
Decimal / Double |
No complex_data_type_name needed |
boolean |
lightning__booleanType |
Boolean |
No complex_data_type_name needed |
datetime |
lightning__dateTimeStringType |
DateTime |
Actions only — not valid for variables |
date |
lightning__dateType |
Date |
Valid for both variables and actions |
currency |
lightning__currencyType |
Decimal |
Annotated with currency type |
Pro Tip: Don't manually edit complex_data_type_name - use the UI dropdown in Agentforce Assets > Action Definition, then export/import the action definition.
⚠️ Canvas View Corruption Bugs
CRITICAL: Canvas view can silently corrupt Agent Script syntax. Make complex edits in Script view.
| Original Syntax | Canvas Corrupts To | Impact |
|---|---|---|
== |
{! OPERATOR.EQUAL } |
Breaks conditionals |
if condition: |
if condition (missing colon) |
Parse error |
with email = |
with @inputs.email = |
Invalid syntax |
| 4-space indent | De-indented (breaks nesting) | Structure lost |
@topic.X (supervision) |
@utils.transition to @topic.X (handoff) |
Changes return behavior |
A and B |
A {! and } B |
Breaks compound conditions |
Safe Workflow:
- Use Script view for all structural edits (conditionals, actions, transitions)
- Use Canvas only for visual validation and simple text changes
- Always review in Script view after any Canvas edit
⚠️ Preview Mode Critical Bugs
CRITICAL REFRESH BUG: Browser refresh required after every Agent Script save before preview works properly.
| Issue | Error Message | Workaround |
|---|---|---|
| Linked vars in context, not state | "Cannot access 'X': Not a declared field in dict" |
Convert to mutable + hardcode for testing |
| Output property access fails | Silent failure, no error | Assign to variable first, then use in conditional |
| Simulate vs Live behavior differs | Works in Simulate, fails in Live | Test in BOTH modes before committing |
Pattern for Testing Linked Variables:
# ❌ DOESN'T WORK IN PREVIEW (linked var from session):
RoutableId: linked string
source: @MessagingSession.Id
# ✅ WORKAROUND FOR TESTING (hardcode value):
RoutableId: mutable string = "test-session-123"
description: "MessagingSession Id (hardcoded for testing)"
# After testing, switch back to linked for production
Output Property Access Pattern:
# ❌ DOESN'T WORK IN PREVIEW (direct output access):
if @actions.check_status.result == "approved":
| Approved!
# ✅ CORRECT (assign to variable first):
set @variables.status = @outputs.result
if @variables.status == "approved":
| Approved!
if/else Nesting Rules — Two Valid Approaches
# ❌ WRONG - `else if` is NOT valid syntax
if @variables.tier == "gold":
| Gold tier detected.
else if @variables.tier == "silver": # SyntaxError!
| Silver tier detected.
# ❌ WRONG - Direct nested if inside if (no else:) causes SyntaxError
if @variables.software_cost > 0:
if @variables.software_cost <= 500: # SyntaxError!
| Auto-approve this software request.
# ❌ WRONG - else: with nested if ALSO causes SyntaxError (TDD validated 2026-02-14)
if @variables.is_member == True:
| Welcome back, member!
else:
if @variables.visit_count > 5: # SyntaxError: Unexpected 'if'
| Welcome back, frequent visitor!
# ✅ CORRECT Approach 1 - Compound condition (when logic allows)
if @variables.software_cost > 0 and @variables.software_cost <= 500:
| Auto-approve this software request.
# ✅ CORRECT Approach 2 - Flatten to sequential ifs (for separate messages)
if @variables.order_verified == False or @variables.payment_confirmed == False:
| ❌ **PROCESSING BLOCKED**
| Missing requirements:
if @variables.order_verified == False:
| - Order verification pending
if @variables.payment_confirmed == False:
| - Payment confirmation pending
Summary:
else ifis NOT valid. Directifinsideifis NOT valid.else:with nestedifis also NOT valid (TDD disproved 2026-02-14). Use compound conditions for multi-branch logic, or flatten to sequential ifs for separate outputs.
... is Slot-Filling Syntax (LLM Extracts from Conversation)
# ❌ WRONG - Using ... as default value
order_id: mutable string = ...
# ✅ CORRECT - Use ... only in action parameter binding
reasoning:
actions:
search: @actions.search_products
with query=... # LLM extracts from user message
with category=... # LLM decides based on context
with limit=10 # Fixed value
Post-Action Directives: Only on @actions.*
# ❌ WRONG - @utils does NOT support set/run/if
go_next: @utils.transition to @topic.main
set @variables.visited = True # ERROR!
# ✅ CORRECT - Only @actions.* supports post-action
process: @actions.process_order
with order_id=@variables.order_id
set @variables.status = @outputs.status # ✅ Works
run @actions.send_notification # ✅ Works
if @outputs.needs_review: # ✅ Works
transition to @topic.review
Helper Topic Pattern (For Demo Agents Without Flows/Apex)
When you need to set variables without backend actions, use dedicated "helper topics":
# Main topic offers LLM-selectable action
topic verify_employee:
reasoning:
actions:
complete_verification: @utils.transition to @topic.verification_success
description: "Mark employee as verified"
available when @variables.employee_verified == False
# Helper topic sets variables in instructions, then returns
topic verification_success:
description: "Set verified state and return"
reasoning:
instructions: ->
set @variables.employee_verified = True
set @variables.employee_name = "Demo Employee"
| ✓ Identity verified!
transition to @topic.verify_employee # Return to parent
Why this works:
setstatements ARE valid insideinstructions: ->blocks. The topic loop pattern lets you change state without Flows/Apex.
💰 PRODUCTION GOTCHAS: Billing, Determinism & Performance
Credit Consumption Table
Key insight: Framework operations are FREE. Only actions that invoke external services consume credits.
| Operation | Credits | Notes |
|---|---|---|
@utils.transition |
FREE | Framework navigation |
@utils.setVariables |
FREE | Framework state management |
@utils.escalate |
FREE | Framework escalation |
if/else control flow |
FREE | Deterministic resolution |
before_reasoning |
FREE | Deterministic pre-processing (see note below) |
after_reasoning |
FREE | Deterministic post-processing (see note below) |
reasoning (LLM turn) |
FREE | LLM reasoning itself is not billed |
| Prompt Templates | 2-16 | Per invocation (varies by complexity) |
| Flow actions | 20 | Per action execution |
| Apex actions | 20 | Per action execution |
| Any other action | 20 | Per action execution |
✅ Lifecycle Hooks Validated (v1.3.0): The
before_reasoning:andafter_reasoning:lifecycle hooks are now TDD-validated. Content goes directly under the block (noinstructions:wrapper). See "Lifecycle Hooks" section below for correct syntax.
Cost Optimization Pattern: Fetch data once in before_reasoning:, cache in variables, reuse across topics.
Lifecycle Hooks: before_reasoning: and after_reasoning:
TDD Validated (2026-01-20): These hooks enable deterministic pre/post-processing around LLM reasoning.
topic main:
description: "Topic with lifecycle hooks"
# BEFORE: Runs deterministically BEFORE LLM sees instructions
before_reasoning:
# Content goes DIRECTLY here (NO instructions: wrapper!)
set @variables.pre_processed = True
set @variables.customer_tier = "gold"
# LLM reasoning phase
reasoning:
instructions: ->
| Customer tier: {!@variables.customer_tier}
| How can I help you today?
# AFTER: Runs deterministically AFTER LLM finishes reasoning
after_reasoning:
# Content goes DIRECTLY here (NO instructions: wrapper!)
set @variables.interaction_logged = True
if @variables.needs_audit == True:
set @variables.audit_flag = True
Key Points:
- Content goes directly under
before_reasoning:/after_reasoning:(NOinstructions:wrapper) - Supports
set,if,runstatements (same as proceduralinstructions: ->) before_reasoning:is FREE (no credit cost) - use for data prepafter_reasoning:is FREE (no credit cost) - use for logging, cleanup
❌ WRONG Syntax (causes compile error):
before_reasoning:
instructions: -> # ❌ NO! Don't wrap with instructions:
set @variables.x = True
✅ CORRECT Syntax:
before_reasoning:
set @variables.x = True # ✅ Direct content under the block
Supervision vs Handoff (Clarified Terminology)
| Term | Syntax | Behavior | Use When |
|---|---|---|---|
| Handoff | @utils.transition to @topic.X |
Control transfers completely, child generates final response | Checkout, escalation, terminal states |
| Supervision | @topic.X (as action reference) |
Parent orchestrates, child returns, parent synthesizes | Expert consultation, sub-tasks |
# HANDOFF - child topic takes over completely:
checkout: @utils.transition to @topic.order_checkout
description: "Proceed to checkout"
# → @topic.order_checkout generates the user-facing response
# SUPERVISION - parent remains in control:
get_advice: @topic.product_expert
description: "Consult product expert"
# → @topic.product_expert returns, parent topic synthesizes final response
KNOWN BUG: Adding ANY new action in Canvas view may inadvertently change Supervision references to Handoff transitions.
Action Output Flags for Zero-Hallucination Routing
Key Pattern for Determinism: Control what the LLM can see and say.
When defining actions in Agentforce Assets, use these output flags:
| Flag | Effect | Use When |
|---|---|---|
is_displayable: False |
LLM cannot show this value to user | Preventing hallucinated responses |
is_used_by_planner: True |
LLM can reason about this value | Decision-making, routing |
Zero-Hallucination Intent Classification Pattern:
# In Agentforce Assets - Action Definition outputs:
outputs:
intent_classification: string
is_displayable: False # LLM cannot show this to user
is_used_by_planner: True # LLM can use for routing decisions
# In Agent Script - LLM routes but cannot hallucinate:
topic intent_router:
reasoning:
instructions: ->
run @actions.classify_intent
set @variables.intent = @outputs.intent_classification
if @variables.intent == "refund":
transition to @topic.refunds
if @variables.intent == "order_status":
transition to @topic.orders
Action I/O Metadata Properties (TDD Validated v2.2.0)
Complete reference for all metadata properties available on action definitions, inputs, and outputs. These control how the LLM and UI interact with action parameters.
Action-Level Properties:
| Property | Type | Effect | TDD Status |
|---|---|---|---|
label |
String | Display name in UI | ✅ v2.2.0 |
description |
String | LLM reads this for decision-making | ✅ v1.3.0 |
require_user_confirmation |
Boolean | Request user confirmation before execution | ✅ Compiles (runtime Issue 6) |
include_in_progress_indicator |
Boolean | Show spinner during execution | ✅ v2.2.0 |
progress_indicator_message |
String | Custom spinner text | ✅ v2.2.0 |
Input Properties:
| Property | Type | Effect | TDD Status |
|---|---|---|---|
description |
String | Explains parameter to LLM | ✅ v1.3.0 |
label |
String | Display name in UI | ✅ v2.2.0 |
is_required |
Boolean | Marks input as mandatory for LLM | ✅ v2.2.0 |
is_user_input |
Boolean | LLM extracts value from conversation | ✅ v2.2.0 |
complex_data_type_name |
String | Lightning type mapping | ✅ v2.1.0 |
Output Properties:
| Property | Type | Effect | TDD Status |
|---|---|---|---|
description |
String | Explains output to LLM | ✅ v1.3.0 |
label |
String | Display name in UI | ✅ v2.2.0 |
is_displayable |
Boolean | False = hide from user (alias: filter_from_agent) |
✅ v2.2.0 |
is_used_by_planner |
Boolean | True = LLM can reason about value |
✅ v2.2.0 |
complex_data_type_name |
String | Lightning type mapping | ✅ v2.1.0 |
Cross-reference:
filter_from_agent: True(in actions-reference.md) is equivalent tois_displayable: False. Both hide the output from user display.is_displayableis the standard property name.
User Input Pattern (is_user_input: True):
# LLM extracts the value from conversation context rather than asking explicitly
inputs:
customer_name: string
description: "Customer's full name"
is_user_input: True # LLM pulls from what user already said
is_required: True # Must have a value before action executes
Action Chaining with run Keyword
Known quirk: Parent action may complain about inputs needed by chained action - this is expected.
# Chained action execution:
process_order: @actions.create_order
with customer_id = @variables.customer_id
run @actions.send_confirmation # Chains after create_order completes
set @variables.order_id = @outputs.id
KNOWN BUG: Chained actions with Prompt Templates don't properly map inputs using Input:Query format:
# ❌ MAY NOT WORK with Prompt Templates:
run @actions.transform_recommendation
with "Input:Reco_Input" = @variables.ProductReco
# ⚠️ TRY THIS (may still have issues):
run @actions.transform_recommendation
with Reco_Input = @variables.ProductReco
📖 For prompt template action definitions, input binding syntax, and grounded data patterns, see resources/action-prompt-templates.md. For context-aware descriptions, instruction references (
{!@actions.X}), and advanced binding strategies, see resources/action-patterns.md.
Latch Variable Pattern for Topic Re-entry
Problem: Topic selector doesn't properly re-evaluate after user provides missing input.
Solution: Use a "latch" variable to force re-entry:
variables:
verification_in_progress: mutable boolean = False
start_agent topic_selector:
reasoning:
instructions: ->
# LATCH CHECK - force re-entry if verification was started
if @variables.verification_in_progress == True:
transition to @topic.verification
| How can I help you today?
actions:
start_verify: @topic.verification
description: "Start identity verification"
# Set latch when user chooses this action
set @variables.verification_in_progress = True
topic verification:
reasoning:
instructions: ->
| Please provide your email to verify your identity.
actions:
verify: @actions.verify_identity
with email = ...
set @variables.verified = @outputs.success
# Clear latch when verification completes
set @variables.verification_in_progress = False
Loop Protection Guardrail
Agent Scripts have a built-in guardrail that limits iterations to approximately 3-4 loops before breaking out and returning to the Topic Selector.
Best Practice: Map out your execution paths - particularly topic transitions. Ensure testing covers all paths and specifically check for unintended circular references between topics.
Token & Size Limits
| Limit Type | Value | Notes |
|---|---|---|
| Max response size | 1,048,576 bytes (1MB) | Per agent response |
| Plan trace limit (Frontend) | 1M characters | For debugging UI |
| Transformed plan trace (Backend) | 32k tokens | Internal processing |
| Active/Committed Agents per org | 100 max | Org limit |
Progress Indicators
Add user feedback during long-running actions:
actions:
fetch_data: @actions.get_customer_data
description: "Fetch customer information"
include_in_progress_indicator: True
progress_indicator_message: "Fetching your account details..."
VS Code Pull/Push NOT Supported
# ❌ ERROR when using source tracking:
Failed to retrieve components using source tracking:
[SfError [UnsupportedBundleTypeError]: Unsupported Bundle Type: AiAuthoringBundle
# ✅ WORKAROUND - Use CLI directly:
sf project retrieve start -m AiAuthoringBundle:MyAgent
sf agent publish authoring-bundle --api-name MyAgent -o TARGET_ORG
Language Block Quirks
- Hebrew and Indonesian appear twice in the language dropdown
- Selecting from the second set causes save errors
- Use
adaptive_response_allowed: Truefor automatic language adaptation
language:
locale: en_US
adaptive_response_allowed: True # Allow language adaptation
Cross-Skill Orchestration
| Direction | Pattern | Priority |
|---|---|---|
| Before Agent Script | /sf-flow - Create Flows for flow:// action targets |
⚠️ REQUIRED |
| After Agent Script | /sf-ai-agentforce-testing - Test topic routing and actions |
✅ RECOMMENDED |
| For Deployment | /sf-deploy - Publish agent with sf agent publish authoring-bundle |
⚠️ REQUIRED |
Tip: Open Agentforce Studio list view with
sf org open authoring-bundle -o TARGET_ORG(v2.121.7+). Open a specific agent withsf org open agent --api-name MyAgent -o TARGET_ORG.
📋 QUICK REFERENCE: Agent Script Syntax
Block Structure (CORRECTED Order per Official Spec)
config: # 1. Required: Agent metadata (developer_name, agent_type, default_agent_user)
variables: # 2. Optional: State management (mutable/linked)
system: # 3. Required: Global messages and instructions
connection: # 4. Optional: Escalation routing — use `connection messaging:` (singular, NOT `connections:`)
knowledge: # 5. Optional: Knowledge base config
language: # 6. Optional: Locale settings
start_agent: # 7. Required: Entry point (exactly one)
topic: # 8. Required: Conversation topics (one or more)
Config Block Field Names (CRITICAL)
⚠️ Common Error: Using incorrect field names from outdated documentation.
| Documented Field (Wrong) | Actual Field (Correct) | Notes |
|---|---|---|
agent_name |
developer_name |
Must match folder name (case-sensitive) |
description |
agent_description |
Agent's purpose description |
agent_label |
(not used) | Remove from examples |
default_agent_user |
default_agent_user |
✓ Correct |
| (missing) | agent_type |
Required: AgentforceServiceAgent or AgentforceEmployeeAgent |
# ✅ CORRECT config block:
config:
developer_name: "my_agent"
agent_description: "Handles customer support inquiries"
agent_type: "AgentforceServiceAgent"
default_agent_user: "agent_user@00dxx000001234.ext"
Naming Rules (All Identifiers)
- Only letters, numbers, underscores
- Must begin with a letter
- No spaces, no consecutive underscores, cannot end with underscore
- Maximum 80 characters
Instruction Syntax Patterns
| Pattern | Purpose | Example |
|---|---|---|
instructions: | |
Literal multi-line (no expressions) | instructions: | Help the user. |
instructions: -> |
Procedural (enables expressions) | instructions: -> if @variables.x: |
| text |
Literal text for LLM prompt | | Hello + variable injection |
if @variables.x: |
Conditional (resolves before LLM) | if @variables.verified == True: |
run @actions.x |
Execute action during resolution | run @actions.load_customer |
set @var = @outputs.y |
Capture action output | set @variables.risk = @outputs.score |
set @var = value |
Set variable in instructions | set @variables.count = 0 |
{!@variables.x} |
Variable injection in text | Risk score: {!@variables.risk} |
{!expr if cond else alt} |
Conditional interpolation | {!@variables.status if @variables.status else "pending"} |
available when |
Control action visibility to LLM | available when @variables.verified == True |
with param=... |
LLM slot-filling (extracts from conversation) | with query=... |
with param=value |
Fixed parameter value | with limit=10 |
Transition vs Delegation (CRITICAL DISTINCTION)
| Syntax | Behavior | Returns? | Use When |
|---|---|---|---|
@utils.transition to @topic.X |
Permanent handoff | ❌ No | Checkout, escalation, final states |
@topic.X (in reasoning.actions) |
Delegation | ✅ Yes | Get expert advice, sub-tasks |
transition to @topic.X (inline) |
Deterministic jump | ❌ No | Post-action routing, gates |
# Delegation - returns to current topic after specialist finishes
consulting: @topic.expert_topic
description: "Get expert advice"
# Transition - permanent handoff, no return
checkout: @utils.transition to @topic.checkout
description: "Proceed to purchase"
Expression Operators (Safe Subset)
| Category | Operators | NOT Supported |
|---|---|---|
| Comparison | ==, != (not-equal), <, <=, >, >=, is, is not |
❌ <> (not valid, use !=) |
| Logical | and, or, not |
|
| Arithmetic | +, - |
❌ *, /, % |
| Access | . (property), [] (index) |
|
| Conditional | x if condition else y |
Variable Types
| Modifier | Behavior | Supported Types | Default Required? |
|---|---|---|---|
mutable |
Read/write state | string, number, boolean, object, date, timestamp, currency, id, list[T] |
✅ Yes |
linked |
Read-only from source | string, number, boolean, date, timestamp, currency, id |
❌ No (has source:) |
⚠️ Linked variables CANNOT use
objectorlisttypes
Linked Variable Sources by Agent Type
⚠️ CRITICAL: Not all source bindings work for all agent types.
| Source Pattern | Service Agent | Employee Agent |
|---|---|---|
@MessagingSession.Id |
✅ Works | ❌ Not available |
@MessagingSession.RoutableId |
✅ Works | ❌ Not available |
@Record.Id |
❓ Untested | ❌ Does not work |
@context.recordId |
❓ Untested | ❌ Does not work |
Workaround for Employee Agents: Employee Agents in the Copilot panel don't automatically receive record context. Use a mutable variable and have the Flow action look up the current record.
# ❌ DOESN'T WORK for Employee Agents:
case_id: linked string
source: @Record.Id
# ✅ WORKAROUND - use mutable variable:
case_id: mutable string = ""
description: "Case ID - enter or will be looked up by Flow"
Variable vs Action I/O Type Matrix
Critical distinction: Some types are valid ONLY for action inputs/outputs, NOT for Agent Script variables.
| Type | Variables | Action I/O | Notes |
|---|---|---|---|
string |
✅ | ✅ | Universal |
number |
✅ | ✅ | Universal |
boolean |
✅ | ✅ | Universal |
date |
✅ | ✅ | Universal |
currency |
✅ | ✅ | Universal |
id |
✅ | ✅ | Salesforce IDs |
list |
✅ (mutable only) | ✅ | Collections |
object |
✅ (mutable only) | ✅ | ⚠️ Not for linked vars |
datetime |
❌ | ✅ | Actions only |
time |
❌ | ✅ | Actions only |
integer |
❌ | ✅ | Actions only |
long |
❌ | ✅ | Actions only |
Source: AGENT_SCRIPT.md rules document from trailheadapps/agent-script-recipes
Action Target Protocols
| Short | Long Form | Use When | Validated? |
|---|---|---|---|
flow |
flow:// |
Data operations, business logic | ✅ TDD |
apex |
apex:// |
Custom calculations, validation | ✅ TDD |
prompt |
generatePromptResponse:// |
Grounded LLM responses | ✅ TDD |
api |
api:// |
REST API calls | ✅ TDD |
retriever |
retriever:// |
RAG knowledge search | ✅ TDD |
externalService |
externalService:// |
Third-party APIs via Named Credentials | ✅ TDD |
standardInvocableAction |
standardInvocableAction:// |
Built-in SF actions (email, tasks) | ✅ TDD |
datacloudDataGraphAction |
datacloudDataGraphAction:// |
Data Cloud graph queries | 📋 Spec |
datacloudSegmentAction |
datacloudSegmentAction:// |
Data Cloud segment operations | 📋 Spec |
triggerByKnowledgeSource |
triggerByKnowledgeSource:// |
Knowledge article triggers | 📋 Spec |
contextGrounding |
contextGrounding:// |
Context grounding for LLM | 📋 Spec |
predictiveAI |
predictiveAI:// |
Einstein prediction models | 📋 Spec |
runAction |
runAction:// |
Execute sub-actions | 📋 Spec |
external |
external:// |
External service calls | 📋 Spec |
copilotAction |
copilotAction:// |
Salesforce Copilot actions | 📋 Spec |
@topic.X |
(inline) | Topic delegation (returns to parent) | ✅ TDD |
Legend: ✅ TDD = Validated via deployment testing | 📋 Spec = Documented in AGENT_SCRIPT.md spec (requires specific org setup to test)
Using Flow and Apex Actions in Agent Script
For AiAuthoringBundle (Agent Script):
flow://andapex://targets work directly — no GenAiFunction registration needed. The target just needs to exist in the org (active Flow or deployed Apex class with@InvocableMethod).
Two-Level Action System (CRITICAL to understand):
Level 1: ACTION DEFINITION (in topic's `actions:` block)
→ Has `target:`, `inputs:`, `outputs:`, `description:`
→ Specifies WHAT to call (e.g., "apex://OrderService")
Level 2: ACTION INVOCATION (in `reasoning.actions:` block)
→ References Level 1 via `@actions.name`
→ Specifies HOW to call it (with/set clauses)
Complete Example:
topic order_status:
description: "Look up order details"
# Level 1: DEFINE the action with a target
actions:
get_case_details:
description: "Fetch case information by ID"
inputs:
case_id: string
description: "The Case record ID"
outputs:
subject: string
description: "Case subject line"
target: "flow://Get_Case_Details" # Flow must exist and be active
reasoning:
instructions: |
Help the customer check their case status.
# Level 2: INVOKE the action defined above
actions:
lookup_case: @actions.get_case_details
with case_id = @variables.case_id
set @variables.case_subject = @outputs.subject
⚠️ PRODUCTION INSIGHT: I/O Schemas Are REQUIRED for Publish
Action definitions with only
description:andtarget:(noinputs:/outputs:) will PASS LSP and CLI validation but FAIL server-side compilation with "Internal Error." Always include complete I/O schemas in Level 1 action definitions. Specifically, theoutputs:block is required — havinginputs:but nooutputs:still triggers the Internal Error (TDD v2.1.0: Val_No_Outputs).This was previously misattributed to a "flow:// target bug." The actual root cause is missing type contracts — adding
inputs:andoutputs:blocks resolves the issue for bothflow://andapex://targets.
⚠️ Level 1 Without Level 2 Is Valid (TDD v2.1.0)
You do NOT need a Level 2
@actions.Xinvocation for every Level 1 action definition. Defining an action in the topic'sactions:block withtarget:,inputs:,outputs:is sufficient — the LLM auto-selects from available actions based on their descriptions. Val_Level1_Only published successfully with ONLY Level 1 definitions.
I/O Name Matching Rules (TDD Validated v2.1.0):
Action input/output names in Agent Script MUST exactly match the
@InvocableVariablefield names in the Apex target.
| Scenario | Agent Script Name | Apex Field Name | Result |
|---|---|---|---|
| ✅ Exact match | inputText |
inputText |
Publishes |
| ❌ Wrong name | wrong_name |
outputText |
"invalid output 'wrong_name'" |
| ✅ Partial outputs | Declare only outputText |
Has outputText + more |
Publishes (subset OK) |
| ❌ Bare @InvocableMethod | ANY name | List<String> param (no wrapper) |
Always fails — no discoverable name |
Bare @InvocableMethod Pattern (NOT Compatible with Agent Script):
⚠️ CRITICAL: Apex methods returning
List<String>directly (without@InvocableVariablewrapper classes) CANNOT be used as Agent Script action targets. The framework has no discoverable parameter names to bind against.
// ❌ DOES NOT WORK with Agent Script:
@InvocableMethod
public static List<String> greet(List<String> names) { ... }
// → No input name is discoverable — "name", "input", "names" all fail
// ✅ ALWAYS use wrapper classes for Agent Script:
public class GreetInput {
@InvocableVariable(label='Name' required=true)
public String userName; // ← Agent Script uses "userName" as input name
}
public class GreetOutput {
@InvocableVariable(label='Greeting')
public String greeting; // ← Agent Script uses "greeting" as output name
}
@InvocableMethod
public static List<GreetOutput> greet(List<GreetInput> inputs) { ... }
Partial Output Pattern:
You can declare a SUBSET of the target's outputs in your Agent Script action definition. You do NOT need to declare every output field from the Apex class. Val_Partial_Output confirmed: declaring only
outputTextfromTestApexAction(which has a full output wrapper) publishes successfully.
⚠️ Common Error:
ValidationError: Tool target 'X' is not an action definition— This means either:
- The action is referenced in
reasoning.actions:via@actions.XbutXis not defined in the topic'sactions:block, OR- The
target:value points to a Flow/Apex class that doesn't exist in the orgFix: Ensure you have BOTH levels: action definition (with
target:) AND action invocation (with@actions.name).
Agent Builder UI Path (GenAiPlannerBundle — different workflow):
If building agents through the Agent Builder UI (not Agent Script), you DO need GenAiFunction metadata. See resources/actions-reference.md for details.
Connection Block (Escalation Routing — Beta Feature)
⚠️ Service Agents Only: The
connectionblock is only valid foragent_type: "AgentforceServiceAgent". Employee Agents do not support channel-based escalation routing.
⚠️ CRITICAL SYNTAX: Use
connection messaging:(singular, NO wrapper block). Theconnections:(plural) wrapper from some docs/recipes does NOT compile. Eachconnection <channel>:is a standalone top-level block.
⚠️
outbound_route_namerequiresflow://prefix: Using a bare Flow API name (e.g.,"My_Flow") causesERROR_HTTP_404on publish. Must use"flow://My_Flow"format.
Minimal form (no routing — just enables the channel):
connection messaging:
adaptive_response_allowed: True
Full form with escalation routing (production-validated on Vivint-DevInt, 2026-02-16):
connection messaging:
outbound_route_type: "OmniChannelFlow"
outbound_route_name: "flow://Route_from_Vivint_Virtual_Support"
escalation_message: "One moment while I connect you with a support specialist."
adaptive_response_allowed: False
All-or-nothing rule: When outbound_route_type is present, ALL three route properties are required (outbound_route_type, outbound_route_name, escalation_message). Omitting any one causes validation failure.
Key Properties:
| Property | Required | Description |
|---|---|---|
outbound_route_type |
Conditional | "OmniChannelFlow", "Queue", or "Skill". Required if routing is configured. |
outbound_route_name |
Conditional | Must use flow:// prefix for OmniChannelFlow (e.g., "flow://My_Flow"). Required with outbound_route_type. |
escalation_message |
Conditional | Message shown to user during handoff. Required with outbound_route_type. |
| `adaptive_response_allowed |
…(truncated)