13 KiB
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.
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:(NOinstructions:wrapper) - Supported primitives include
set,if/else,transition to, andrun @actions.Xwith 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 prepafter_reasoning:is FREE (no credit cost) - use for logging, cleanuptransition toworks inafter_reasoning:— but if a subagent transitions mid-reasoning, the original subagent'safter_reasoning:does NOT run
❌ WRONG Syntax (causes compile error):
before_reasoning:
instructions: -> # ❌ NO! Don't wrap with instructions:
transition to @subagent.verification
✅ CORRECT Syntax:
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 |
# 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.
# 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:
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.
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.
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
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 --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.
# 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'(ordescription,label) - Fix: Rename the
@InvocableVariablein 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: Truefor automatic language adaptation