afv-library/skills/agentforce-generate/references/production-gotchas.md

302 lines
13 KiB
Markdown

# Production Gotchas: Billing, Determinism & Performance
Credit consumption, lifecycle hooks, determinism patterns, and performance guardrails in Agentforce.
---
## Credit Consumption Table
| 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 |
The `before_reasoning:` and `after_reasoning:` lifecycle hooks are validated. Content goes **directly** under the block (no `instructions:` wrapper). See "Lifecycle Hooks" section below for correct syntax.
### Cost Optimization Pattern
Cache only exact external data that a named later action, guard, or instruction
must consume. Define refresh, expiry, correction, and reset behavior. Do not
cache conversational facts merely because `setVariables` is free; surviving
history already carries those facts.
## Lifecycle Hooks
This example assumes `verified_customer_id` is written only by a successful
verification action and `commit_failed` is written by the protected action.
The named consumers are the authorization guard and failure transition.
```yaml
subagent protected_operation:
description: "Subagent with lifecycle hooks"
# BEFORE: Runs deterministically BEFORE LLM sees instructions
before_reasoning:
# Content goes DIRECTLY here (NO instructions: wrapper!)
if @variables.verified_customer_id == "":
transition to @subagent.verification
# LLM reasoning phase
reasoning:
instructions: | Help the verified customer with the protected operation.
# AFTER: Runs deterministically AFTER LLM finishes reasoning
after_reasoning:
# Content goes DIRECTLY here (NO instructions: wrapper!)
if @variables.commit_failed == True:
transition to @subagent.operation_recovery
```
**Key Points:**
- Content goes **directly** under `before_reasoning:` / `after_reasoning:` (NO `instructions:` wrapper)
- Supported primitives include `set`, `if`/`else`, `transition to`, and
`run @actions.X` with callbacks. Validate the referenced action and its
bindings with the target bundle's full parser/linter.
- `before_reasoning:` is FREE (no credit cost) - use for data prep
- `after_reasoning:` is FREE (no credit cost) - use for logging, cleanup
- `transition to` works in `after_reasoning:` — but if a subagent transitions mid-reasoning, the original subagent's `after_reasoning:` does NOT run
**❌ WRONG Syntax (causes compile error):**
```yaml
before_reasoning:
instructions: -> # ❌ NO! Don't wrap with instructions:
transition to @subagent.verification
```
**✅ CORRECT Syntax:**
```yaml
before_reasoning:
transition to @subagent.verification # ✅ Direct content under the block
```
## Supervision vs Handoff
| Term | Syntax | Behavior | Use When |
|------|--------|----------|----------|
| **Handoff** | `@utils.transition to @subagent.X` | Control transfers completely, child generates final response | Checkout, escalation, terminal states |
| **Supervision** | `@subagent.X` (as action reference) | Parent orchestrates, child returns, parent synthesizes | Expert consultation, sub-tasks |
```yaml
# HANDOFF - child subagent takes over completely:
checkout: @utils.transition to @subagent.order_checkout
description: "Proceed to checkout"
# → @subagent.order_checkout generates the user-facing response
# SUPERVISION - parent remains in control:
get_advice: @subagent.product_expert
description: "Consult product expert"
# → @subagent.product_expert returns, parent subagent 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
Control what the LLM can see and say.
When defining actions in Agentforce Assets, use these output flags:
| Flag | Effect | Use When |
|------|--------|----------|
| `filter_from_agent: True` | LLM **cannot** show this value to user | Preventing hallucinated responses (GA standard) |
| `is_used_by_planner: True` | LLM **can** reason about this value | Decision-making, routing |
**Zero-Hallucination Intent Classification Pattern:**
Use this only when a reproduced routing failure justifies an external
classifier. `intent` is exact classifier output consumed immediately by the
shown transitions; do not preserve it as a conversation-focus latch.
```yaml
# In Agentforce Assets - Action Definition outputs:
outputs:
intent_classification: string
filter_from_agent: True # LLM cannot show this to user (GA standard)
is_used_by_planner: True # LLM can use for routing decisions
# In Agent Script - LLM routes but cannot hallucinate:
subagent intent_router:
reasoning:
instructions: ->
run @actions.classify_intent
set @variables.intent = @outputs.intent_classification
if @variables.intent == "refund":
transition to @subagent.refunds
if @variables.intent == "order_status":
transition to @subagent.orders
```
## Action I/O Metadata Properties
Complete reference for all metadata properties available on action definitions, inputs, and outputs.
**Action-Level Properties:**
| Property | Type | Effect |
|----------|------|--------|
| `label` | String | Display name in UI |
| `description` | String | LLM reads this for decision-making |
| `require_user_confirmation` | Boolean | Request user confirmation before execution (compiles; runtime no-op per Issue 6) |
| `include_in_progress_indicator` | Boolean | Show spinner during execution |
| `progress_indicator_message` | String | Custom spinner text |
**Input Properties:**
| Property | Type | Effect |
|----------|------|--------|
| `description` | String | Explains parameter to LLM |
| `label` | String | Display name in UI |
| `is_required` | Boolean | Marks input as mandatory for LLM |
| `is_user_input` | Boolean | LLM extracts value from conversation |
| `complex_data_type_name` | String | Lightning type mapping |
**Output Properties:**
| Property | Type | Effect |
|----------|------|--------|
| `description` | String | Explains output to LLM |
| `label` | String | Display name in UI |
| `filter_from_agent` | Boolean | `True` = hide from user display (GA standard) |
| `is_displayable` | Boolean | `False` = hide from user (compile-valid alias) |
| `is_used_by_planner` | Boolean | `True` = LLM can reason about value |
| `developer_name` | String | Overrides the parameter's developer name |
| `complex_data_type_name` | String | Lightning type mapping |
`filter_from_agent: True` is the GA standard name. `is_displayable: False` is a compile-valid alias.
### User Input Pattern
With `is_user_input: True`:
```yaml
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
Parent action may complain about inputs needed by chained action - this is expected.
```yaml
process_order: @actions.create_order
with customer_id = @variables.verified_customer_id
run @actions.send_confirmation # Chains after create_order completes
```
Here `verified_customer_id` is trusted verification output consumed by the
protected order action. Do not persist the created order ID unless a named later
action, idempotency check, or response needs that exact value.
KNOWN BUG: Chained actions with Prompt Templates don't properly map inputs using `Input:Query` format.
For prompt template action definitions, input binding syntax, and grounded data patterns, see [Action Prompt Templates](action-prompt-templates.md).
## Focus Locks and Re-entry Latches
Do not add a latch merely to keep ordinary follow-up turns in a subagent. It
duplicates conversation history and can make stale intent override the user's
latest request.
Use a focus lock only after a reproduced routing trace proves it is necessary.
Document its owner, writer, reader, reset, expiry, correction behavior, and
cancel path. The next user turn must be able to cancel or change intent. Keep
proof state such as `verified` separate: trusted authorization output may
remain required even when a conversational focus lock is not.
## 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 Subagent Router.
**Best Practice**: Map out your execution paths and test for unintended circular references between subagents.
## 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
```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 --json -m AiAuthoringBundle:MyAgent
sf agent publish authoring-bundle --json --api-name MyAgent -o TARGET_ORG
```
## `@inputs` Scope Lifecycle (Silent Failure)
`@inputs` is only available in `with` directives during action invocation. Using `@inputs` in a post-action `set` causes **silent runtime failure** — the action executes but the `set` silently drops, leaving the variable unchanged. No trace error; the FunctionStep shows no output capture.
```agentscript
# WRONG — silent failure, @inputs out of scope after action executes
run @actions.get_station_status
with station_name = ...
set @variables.station = @inputs.station_name # FAILS SILENTLY
# RIGHT — consume output only in its valid immediate scope
run @actions.get_station_status
with station_name = ...
if @outputs.status == "closed":
transition to @subagent.station_closed
```
If a later deterministic consumer needs the exact station, make the action
return a canonical station identifier and persist that trusted output with its
consumer and lifecycle documented. Otherwise, leave the user-provided station
in conversation history.
**Diagnosis:** A FunctionStep that completes with no output capture (set directives dropped) indicates an `@inputs` scope violation. The action succeeds — only the assignment fails.
Similarly, `@outputs` is only available in `set` and `if` directives immediately following the action invocation — not in instructions, pipe lines, or later actions.
## Reserved `@InvocableVariable` Keywords
Certain common words cannot be used as `@InvocableVariable` names in Apex classes called by Agent Script. Using them causes "SyntaxError: Unexpected '{keyword}'" during agent script compilation. (Validated March 2026)
**Reserved names (cannot use as `@InvocableVariable`):**
| Reserved Name | Workaround | Example |
|---------------|------------|---------|
| `model` | `vehicle_model`, `data_model`, `model_name` | `@InvocableVariable public String vehicle_model;` |
| `description` | `issue_description`, `desc_text`, `description_field` | `@InvocableVariable public String issue_description;` |
| `label` | `label_text`, `display_label`, `label_field` | `@InvocableVariable public String label_text;` |
**How it manifests:**
- Apex compiles and deploys successfully (these are valid Apex identifiers)
- Error only appears when the Agent Script compiler processes the action's I/O schema
- Error message: `SyntaxError: Unexpected 'model'` (or `description`, `label`)
- Fix: Rename the `@InvocableVariable` in Apex, redeploy, then republish the agent
## 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