# Sf AI Agentscript

> Agent Script DSL development skill for Salesforce Agentforce. Enables writing deterministic agents in a single .agent file with FSM architecture, instruction resolution, and hybrid reasoning. Covers syntax, debugging, testing, and CLI deployment.

- Skill: `tools-only/sf-ai-agentscript-3` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds add tools-only/sf-ai-agentscript-3`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/sf-ai-agentscript-3/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/tools-only/sf-ai-agentscript-3

---


# 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
1. **`default_agent_user` MUST be valid** - Query: `SELECT Username FROM User WHERE Profile.Name = 'Einstein Agent User' AND IsActive = true`
2. **No mixed tabs/spaces** - Use consistent indentation (2-space, 3-space, or tabs - never mix)
3. **Booleans are capitalized** - Use `True`/`False`, not `true`/`false`
4. **Exactly one `start_agent` block** - 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`, and `label` are reserved as **variable/field names** but are valid as **action I/O metadata properties**. For example, you cannot name a variable `label`, but you CAN use `label:` 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.transition` was 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:**
```yaml
actions:
   go_next: @utils.transition to @topic.next
      description: "Navigate to next topic"   # ✅ ONLY description works
```

**What works on action definitions with `target:`:**
```yaml
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**:
1. Use **Script view** for all structural edits (conditionals, actions, transitions)
2. Use Canvas only for visual validation and simple text changes
3. **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:**
```yaml
# ❌ 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:**
```yaml
# ❌ 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
```yaml
# ❌ 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 if` is NOT valid. Direct `if` inside `if` is NOT valid. `else:` with nested `if` is 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)
```yaml
# ❌ 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.*`
```yaml
# ❌ 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":
```yaml
# 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**: `set` statements ARE valid inside `instructions: ->` 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:` and `after_reasoning:` lifecycle hooks are now TDD-validated. Content goes **directly** under the block (no `instructions:` 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.

```yaml
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:` (NO `instructions:` wrapper)
- Supports `set`, `if`, `run` statements (same as procedural `instructions: ->`)
- `before_reasoning:` is FREE (no credit cost) - use for data prep
- `after_reasoning:` is FREE (no credit cost) - use for logging, cleanup

**❌ WRONG Syntax (causes compile error):**
```yaml
before_reasoning:
   instructions: ->      # ❌ NO! Don't wrap with instructions:
      set @variables.x = True
```

**✅ CORRECT Syntax:**
```yaml
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 |

```yaml
# 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:**
```yaml
# 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 to `is_displayable: False`. Both hide the output from user display. `is_displayable` is the standard property name.

**User Input Pattern** (`is_user_input: True`):
```yaml
# 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.

```yaml
# 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:
```yaml
# ❌ 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](resources/action-prompt-templates.md). For context-aware descriptions, instruction references (`{!@actions.X}`), and advanced binding strategies, see [resources/action-patterns.md](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:

```yaml
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:

```yaml
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

```bash
# ❌ 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: True` for automatic language adaptation

```yaml
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 with `sf org open agent --api-name MyAgent -o TARGET_ORG`.

---

## 📋 QUICK REFERENCE: Agent Script Syntax

### Block Structure (CORRECTED Order per Official Spec)
```yaml
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` |

```yaml
# ✅ 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 |

```yaml
# 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 `object` or `list` types**

### 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.

```yaml
# ❌ 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://` and `apex://` 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:**
```yaml
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:` and `target:` (no `inputs:`/`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, the `outputs:` block is required — having `inputs:` but no `outputs:` 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:` and `outputs:` blocks resolves the issue
> for both `flow://` and `apex://` targets.

> **⚠️ Level 1 Without Level 2 Is Valid (TDD v2.1.0)**
>
> You do NOT need a Level 2 `@actions.X` invocation for every Level 1 action definition.
> Defining an action in the topic's `actions:` block with `target:`, `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 `@InvocableVariable` field 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 `@InvocableVariable` wrapper classes) **CANNOT** be used as Agent Script action targets. The framework has no discoverable parameter names to bind against.

```apex
// ❌ 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 `outputText` from `TestApexAction` (which has a full output wrapper) publishes successfully.

> ⚠️ **Common Error**: `ValidationError: Tool target 'X' is not an action definition` — This means either:
> 1. The action is referenced in `reasoning.actions:` via `@actions.X` but `X` is not defined in the topic's `actions:` block, OR
> 2. The `target:` value points to a Flow/Apex class that doesn't exist in the org
>
> **Fix**: 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 `connection` block is only valid for `agent_type: "AgentforceServiceAgent"`. Employee Agents do not support channel-based escalation routing.

> **⚠️ CRITICAL SYNTAX**: Use `connection messaging:` (singular, NO wrapper block). The `connections:` (plural) wrapper from some docs/recipes does NOT compile. Each `connection <channel>:` is a standalone top-level block.

> **⚠️ `outbound_route_name` requires `flow://` prefix**: Using a bare Flow API name (e.g., `"My_Flow"`) causes `ERROR_HTTP_404` on publish. Must use `"flow://My_Flow"` format.

**Minimal form (no routing — just enables the channel):**
```yaml
connection messaging:
   adaptive_response_allowed: True
```

**Full form with escalation routing (production-validated on Vivint-DevInt, 2026-02-16):**
```yaml
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)
