Patterns & Best Practices
This guide combines pattern selection (what to use) with implementation best practices (how to use it well).
Part 1: Pattern Selection
Pattern Decision Tree
┌─────────────────────────────────────────────────────────────────────────┐
│ PATTERN DECISION TREE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ What problem are you solving? │
│ │ │
│ ├─► "I need to run code BEFORE/AFTER every response" │
│ │ └─► Lifecycle Events Pattern │
│ │ File: templates/patterns/lifecycle-events.agent │
│ │ │
│ ├─► "After action X, Y must ALWAYS happen" │
│ │ └─► Action Callbacks Pattern │
│ │ File: templates/patterns/action-callbacks.agent │
│ │ │
│ ├─► "Go to specialist topic, then return with results" │
│ │ └─► Bidirectional Routing Pattern │
│ │ File: templates/patterns/bidirectional-routing.agent │
│ │ │
│ ├─► "Just starting out - what's the minimum?" │
│ │ └─► Hello World │
│ │ File: templates/agents/hello-world.agent │
│ │ │
│ ├─► "Multiple topics, user chooses where to go" │
│ │ └─► Multi-Topic Router │
│ │ File: templates/agents/multi-topic.agent │
│ │ │
│ └─► "Need input validation before actions" │
│ └─► Error Handling Pattern │
│ File: templates/components/error-handling.agent │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Pattern Details
1. Lifecycle Events
File: templates/patterns/lifecycle-events.agent
Purpose: Execute code automatically before and after every reasoning step.
⚠️ Deployment Note: The
runkeyword in lifecycle blocks is GenAiPlannerBundle only. AiAuthoringBundle supportsbefore_reasoning/after_reasoningwithsetstatements, but NOT therunkeyword.
topic conversation:
before_reasoning:
set @variables.turn_count = @variables.turn_count + 1
run @actions.refresh_context # ⚠️ GenAiPlannerBundle only
with user_id=@variables.EndUserId
set @variables.context = @outputs.fresh_context
reasoning:
instructions: ->
| Turn {!@variables.turn_count}: {!@variables.context}
after_reasoning:
run @actions.log_analytics # ⚠️ GenAiPlannerBundle only
with turn=@variables.turn_count
| ✅ Good Use Case | ❌ Not Ideal For |
|---|---|
| Track conversation metrics | One-time setup (use conditional) |
| Refresh context every turn | Heavy processing (adds latency) |
| Log analytics after each response | Actions that might fail often |
2. Action Callbacks
File: templates/patterns/action-callbacks.agent
Purpose: Chain deterministic follow-up actions using the run keyword.
⚠️ Deployment Note: The
runkeyword is GenAiPlannerBundle only. Agents usingrunwill NOT be visible in Agentforce Studio.
process_order: @actions.create_order
with customer_id=@variables.customer_id
set @variables.order_id = @outputs.order_id
run @actions.send_confirmation # ⚠️ GenAiPlannerBundle only
with order_id=@variables.order_id
run @actions.log_activity # ⚠️ GenAiPlannerBundle only
with event="ORDER_CREATED"
| ✅ Good Use Case | ❌ Not Ideal For |
|---|---|
| Audit logging (must happen) | Optional follow-ups (let LLM decide) |
| Send notification after action | Complex branching logic |
| Chain dependent actions | More than 1 level of nesting |
Critical Rule: Only 1 level of run nesting allowed!
3. Bidirectional Routing
File: templates/patterns/bidirectional-routing.agent
Purpose: Navigate to specialist topic, do work, return with results.
# Main hub stores return address
topic main_hub:
reasoning:
actions:
consult_pricing: @utils.transition to @topic.pricing_specialist
# Specialist records where to return
topic pricing_specialist:
before_reasoning:
set @variables.return_topic = "main_hub"
reasoning:
actions:
return_with_results: @utils.transition to @topic.main_hub
| ✅ Good Use Case | ❌ Not Ideal For |
|---|---|
| "Consult expert" workflows | Simple linear flows |
| Results need to come back | One-way topic changes |
| Complex multi-step processes | Single-topic agents |
4. Multi-Topic Router (Hub-and-Spoke)
File: templates/agents/multi-topic.agent
Purpose: Central topic routes to specialized topics based on user intent.
start_agent topic_selector:
reasoning:
instructions: ->
| Determine what the user needs.
actions:
go_orders: @utils.transition to @topic.orders
go_billing: @utils.transition to @topic.billing
go_support: @utils.transition to @topic.support
| ✅ Good Use Case | ❌ Not Ideal For |
|---|---|
| Multiple distinct use cases | Single-purpose agents |
| Clear routing criteria | Complex interdependencies |
| Modular topic development | Need to share state heavily |
5. Error Handling
File: templates/components/error-handling.agent
Purpose: Validate input before processing, handle failures gracefully.
reasoning:
instructions: ->
if @variables.amount is None:
set @variables.valid = False
| Please provide an amount.
if @variables.amount > 10000:
set @variables.valid = False
| Amount exceeds maximum allowed.
actions:
process: @actions.execute_operation
available when @variables.valid == True
retry: @utils.transition to @topic.validation
available when @variables.operation_failed == True
| ✅ Good Use Case | ❌ Not Ideal For |
|---|---|
| Payment processing | Simple queries |
| Data mutations | Read-only operations |
| Compliance workflows | Low-stakes interactions |
Topic Delegation Pattern (@topic.*)
Delegation allows a topic to "consult" another topic and receive control back, unlike permanent transitions.
# ═══════════════════════════════════════════════════════════
# DELEGATION vs TRANSITION
# ═══════════════════════════════════════════════════════════
#
# DELEGATION (@topic.*): Control CAN return to caller
# TRANSITION (@utils.transition): Control does NOT return
#
# ═══════════════════════════════════════════════════════════
topic main_hub:
description: "Main conversation hub"
reasoning:
actions:
# DELEGATION - specialist returns control when done
consult_billing: @topic.billing_specialist
description: "Ask billing expert for help"
available when @variables.needs_billing_help == True
# TRANSITION - permanent move, no return
go_checkout: @utils.transition to @topic.checkout
# Note: no description on transitions!
topic billing_specialist:
description: "Billing expert that returns to caller"
reasoning:
instructions: ->
| Answer billing questions.
| When done, control returns to main_hub.
| Feature | Delegation (@topic.*) |
Transition (@utils.transition to) |
|---|---|---|
| Returns to caller? | ✅ YES | ❌ NO (permanent) |
Use in reasoning.actions |
✅ YES | ✅ YES |
Use in before/after_reasoning |
❌ NO | ✅ YES (bare syntax) |
| Best for | Consult & return | Menu/workflow navigation |
N-ary Boolean Expressions
AgentScript supports 3 or more conditions chained with and/or:
# ═══════════════════════════════════════════════════════════
# THREE+ CONDITIONS WITH AND
# ═══════════════════════════════════════════════════════════
before_reasoning:
if @variables.is_authenticated and @variables.has_permission and @variables.is_active:
transition to @topic.authorized
# ═══════════════════════════════════════════════════════════
# THREE+ CONDITIONS WITH OR
# ═══════════════════════════════════════════════════════════
before_reasoning:
if @variables.is_admin or @variables.is_moderator or @variables.is_owner:
transition to @topic.elevated_access
# ═══════════════════════════════════════════════════════════
# IN available when CLAUSES
# ═══════════════════════════════════════════════════════════
reasoning:
actions:
process_return: @actions.handle_return
description: "Process customer return request"
available when @variables.eligible == True and @variables.order_id != None and @variables.tier != "basic"
premium_action: @actions.premium_feature
description: "Premium tier feature"
available when @variables.tier == "premium" or @variables.tier == "enterprise" or @variables.is_trial_premium == True
Key Points:
- Chain as many conditions as needed with
andoror - Use
()grouping for complex expressions:(a and b) or (c and d) - Works in
ifstatements andavailable whenclauses
Combining Patterns
Patterns can be combined for complex scenarios:
┌─────────────────────────────────────────────────────────────┐
│ LIFECYCLE + CALLBACKS + ROUTING │
├─────────────────────────────────────────────────────────────┤
│ │
│ topic order_hub: │
│ before_reasoning: ◄── Lifecycle │
│ set @variables.turn_count = ... + 1 │
│ │
│ reasoning: │
│ actions: │
│ process: @actions.create │
│ run @actions.notify ◄── Callback │
│ run @actions.log │
│ │
│ consult: @utils.transition ◄── Routing │
│ to @topic.specialist │
│ │
│ after_reasoning: ◄── Lifecycle │
│ run @actions.update_metrics │
│ │
└─────────────────────────────────────────────────────────────┘
Part 2: Best Practices
1. Structure & Organization
Use Meaningful Names
# ✅ GOOD - Clear, descriptive names
topic order_management:
description: "Handles order creation, updates, and status inquiries"
# ❌ BAD - Vague names
topic topic1:
description: "Does stuff"
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Agent name | PascalCase with underscores | Customer_Service_Agent |
| Topic name | snake_case | order_management |
| Variable name | snake_case | user_email |
| Action name | snake_case | get_account_details |
Keep Topics Focused
Each topic should handle ONE category of tasks:
# ✅ GOOD - Single responsibility
topic billing_inquiries:
description: "Answers questions about invoices, payments, and account balances"
topic order_tracking:
description: "Provides order status and shipping updates"
# ❌ BAD - Too broad
topic customer_stuff:
description: "Handles billing, orders, support, and everything else"
2. Variable Management
Initialize Variables with Defaults (Recommended)
variables:
# ✅ RECOMMENDED - Has default value (clearer intent)
user_name: mutable string = ""
description: "Customer's full name"
order_count: mutable number = 0
description: "Number of orders in cart"
# ✅ ALSO VALID - Works but less explicit
# user_name: mutable string
# description: "Customer's full name"
Note: Variables without defaults ARE supported. However, providing defaults is recommended for clarity.
Use Appropriate Types
| Data | Type | Example |
|---|---|---|
| Names, IDs, text | string |
"John Doe" |
| Counts, amounts | number |
42, 99.99 |
| Flags, toggles | boolean |
True, False |
Document Every Variable
variables:
cart_total: mutable number = 0
description: "Total value of items in cart, in USD"
needs_approval: mutable boolean = False
description: "Whether the order requires manager approval (>$10,000)"
3. Topic Design
Start with a Topic Selector
start_agent topic_selector:
description: "Routes users to the appropriate topic based on their needs"
reasoning:
instructions: ->
| Determine what the user needs help with.
| Ask clarifying questions if the intent is unclear.
actions:
orders: @utils.transition to @topic.order_management
description: "Help with orders and purchases"
support: @utils.transition to @topic.technical_support
description: "Technical issues and troubleshooting"
Provide Clear Descriptions
# ✅ GOOD - Specific and actionable
topic password_reset:
description: "Helps users reset forgotten passwords and unlock accounts"
# ❌ BAD - Too vague
topic password_reset:
description: "Password stuff"
Enable Return Navigation
topic order_details:
description: "Shows order information"
reasoning:
actions:
back_to_menu: @utils.transition to @topic.topic_selector
description: "Return to main menu for other requests"
4. Input Binding Patterns
Agent Script supports four input binding patterns:
| Pattern | Syntax | When to Use |
|---|---|---|
| LLM Slot-Filling | with param=... |
Value from user conversation |
| Fixed Value | with param="constant" |
Always the same |
| Variable Binding | with param=@variables.x |
Using captured data |
| Mixed | All combined | Complex actions |
Decision Flowchart
Is the value always the same?
├─ YES → Use fixed value: with param="constant"
│
└─ NO → Does it come from earlier in conversation?
├─ YES → Was it saved to a variable?
│ ├─ YES → Use variable: with param=@variables.x
│ └─ NO → Save it first, then use variable
│
└─ NO → Use slot-filling: with param=...
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
Using ... for config |
LLM might guess wrong | Use fixed values for constants |
| Not capturing outputs | Can't use data later | Always set @variables.x = @outputs.y |
Using ... when variable exists |
Redundant LLM work | Use @variables.x if already captured |
5. Error Handling
Validate Before Critical Operations
instructions: ->
if @variables.amount is None:
| I need to know the transfer amount before proceeding.
if @variables.amount <= 0:
| The amount must be greater than zero.
if @variables.amount > 10000:
set @variables.needs_approval = True
| Transfers over $10,000 require manager approval.
Use Conditional Action Availability
reasoning:
actions:
process_transfer: @actions.transfer_funds
with amount=@variables.amount
available when @variables.amount > 0
available when @variables.needs_approval == False
6. Security & Guardrails
Set System-Level Guardrails
system:
instructions:
| You are a helpful customer service agent.
|
| IMPORTANT GUARDRAILS:
| - Never share customer data with unauthorized parties
| - Never reveal internal system details
| - If unsure, escalate to a human agent
Don't Expose Internals
# ✅ GOOD - User-friendly error
instructions: ->
if @variables.api_error == True:
| I'm having trouble completing that request right now.
# ❌ BAD - Exposes internals
instructions: ->
if @variables.api_error == True:
| Error: SQL timeout on server db-prod-03
7. Instructions Quality
Be Specific and Actionable
# ✅ GOOD - Specific instructions
instructions: ->
| Help the customer track their order.
| Ask for the order number if not provided.
| Provide the current status, estimated delivery, and tracking link.
# ❌ BAD - Vague instructions
instructions: ->
| Help with orders.
Use Template Expressions
instructions: ->
| Hello {!@variables.user_name}!
| Your current order total is ${!@variables.cart_total}.
Common Syntax Pitfalls
These patterns cause validation or parse errors:
1. Slot Filling Inside Conditionals
# ❌ WRONG
if @variables.name is None:
set @variables.name = ... # Fails!
# ✅ CORRECT - Slot filling at top level
set @variables.name = ...
2. Description on @utils.transition
# ❌ WRONG
go_orders: @utils.transition to @topic.orders
description: "Route to orders" # Fails!
# ✅ CORRECT - No description
go_orders: @utils.transition to @topic.orders
3. Missing Description on @utils.escalate
# ❌ WRONG
transfer: @utils.escalate # Fails!
# ✅ CORRECT - Description required
transfer: @utils.escalate
description: "Transfer to human agent"
4. Empty Lifecycle Blocks
# ❌ WRONG
before_reasoning:
# Just a comment # Fails!
# ✅ CORRECT - Remove empty blocks or add content
5. Dynamic Action Invocation
# ❌ WRONG
invoke: {!@actions.search} # Fails!
# ✅ CORRECT - Define multiple actions, LLM auto-selects
search_products: @actions.product_search
search_orders: @actions.order_search
Validation Scoring Summary
| Pattern | Points | Key Requirement |
|---|---|---|
| Config block | 10 | All 4 required fields |
| Linked variables | 10 | EndUserId, RoutableId, ContactId |
| Topic structure | 10 | label, description, reasoning |
| Language block | 5 | default_locale present |
| Lifecycle blocks | 5 | Proper before/after structure |
| Action callbacks | 5 | No nested run |
| Error handling | 5 | Validation patterns |
| Template expressions | 5 | {!@variables.x} syntax |
Quick Reference Checklist
Before deploying an agent, verify:
- All topics have clear descriptions
- All variables have descriptions and defaults
- All actions have input/output descriptions
- System guardrails are defined
- Error handling is in place for critical operations
- Navigation back to main menu from all topics
- Template expressions use correct syntax
{!@variables.name} - Consistent indentation (tabs recommended)
Slot Filling Reliability Patterns
Problem: LLM Fails to Extract Values Correctly
Slot filling (with param=...) relies on LLM inference, which can fail in subtle ways:
| Symptom | What Happened | Example |
|---|---|---|
Empty JSON {} sent to action |
LLM couldn't find value in conversation | User said "look up my account" without ID |
| Wrong field names | LLM abbreviated or guessed | _id instead of account_id |
| Wrong value extracted | LLM picked similar value from context | Picked Contact ID instead of Account ID |
| Retry/crash cycles | No recovery path after failure | Agent keeps trying same extraction |
Root Cause
The ... syntax is probabilistic - the LLM infers what value to use. For critical inputs (IDs, amounts, required fields), this unreliability causes downstream failures.
Solution: Deterministic Critical Input Collection
Instead of relying on slot filling for critical inputs, use this 5-pattern approach:
Pattern 1: First-Interaction Collection
Tell the LLM its PRIMARY GOAL is to collect the input:
reasoning:
instructions: ->
| YOUR PRIMARY GOAL: Collect the account ID from the user.
| Do NOT proceed with any other actions until account_id is captured.
|
if @variables.account_id == "":
| ⚠️ Account ID not yet collected. ASK the user for it.
Why it works: Explicit priority instructions override LLM's tendency to "help" by guessing.
Pattern 2: Variable Setter Action
Create a dedicated action to capture and validate critical input:
actions:
capture_account_id:
description: "Captures and validates the Salesforce Account ID from user"
inputs:
account_id: string
description: "The 18-character Salesforce Account ID (starts with 001)"
is_required: True
outputs:
validated_id: string
description: "The validated account ID (empty if invalid)"
is_valid: boolean
description: "Whether the ID is valid"
target: "flow://Validate_Account_Id"
Why it works: The Flow performs server-side validation, ensuring only valid IDs are stored.
Pattern 3: Single-Use Pattern
Make the setter action unavailable once the variable is set:
reasoning:
actions:
validate_id: @actions.capture_account_id
with account_id=...
set @variables.account_id = @outputs.validated_id
set @variables.account_id_validated = @outputs.is_valid
# ★ SINGLE-USE: Becomes unavailable after successful capture
available when @variables.account_id == ""
Why it works: Prevents redundant re-collection attempts that can introduce errors.
Pattern 4: Null Guard All Downstream Actions
Block ALL other actions until critical input is validated:
reasoning:
actions:
# ★ RESEARCH ACTION - Uses variable, NOT slot filling
do_research: @actions.research_account
with account_id=@variables.account_id # Variable binding!
available when @variables.account_id_validated == True
# ★ TOPIC TRANSITION - Also guarded
go_research: @utils.transition to @topic.research
available when @variables.account_id_validated == True
Why it works: Ensures downstream actions receive valid data, not LLM guesses.
Pattern 5: Explicit Action References in Instructions
Guide the LLM on WHICH action to use:
instructions: ->
| To capture the account ID, use {!@actions.capture_account_id}.
| This ensures the ID is validated before proceeding.
Why it works: Reduces ambiguity about which action handles input collection.
When NOT to Use Slot Filling
Use Slot Filling (...) |
Use Variable/Fixed Value |
|---|---|
| Optional, non-critical inputs | Critical IDs (account, order, case) |
| User preference inputs | Values that must be validated |
| One-time collection | Values used across multiple actions |
| Simple text descriptions | Values with specific formats (dates, IDs) |
Decision Rule: If invalid input would cause downstream failure, use deterministic collection.
Troubleshooting Slot Filling Issues
Symptom: Empty JSON Sent to Action
ERROR: Action received {} instead of {account_id: "001..."}
Cause: LLM couldn't extract value from conversation.
Fix:
- Add explicit collection instructions: "YOUR PRIMARY GOAL: collect account_id"
- Use a dedicated setter action with clear input description
- Add
available when @var != ""guard on downstream actions
Symptom: Wrong Field Names
ERROR: Property _id not found. Expected: account_id
Cause: LLM inferred/abbreviated field name.
Fix:
- Use exact field name in action input description
- Add instruction: "The account_id must be the exact value provided by user"
- Use validation action that normalizes input
Symptom: Agent Retries Then Crashes
Agent attempted 5 times, then failed with: Internal Error
Cause: No recovery path after extraction failure.
Fix:
- Add retry counter:
collection_attempts: mutable number = 0 - Increment on each attempt:
set @var = @var + 1 - Add escape condition:
if @variables.attempts > 3: | Please verify your ID format
Complete Pattern Template
See templates/patterns/critical-input-collection.agent for a complete implementation demonstrating all five patterns together.
Related Documentation
- Agent Script Reference - Complete syntax guide
- Actions Reference - Action integration details
- Prompt Templates - Prompt Template integration