***
title: Conversation Process Best Practices
position: 3
excerpt: >-
Four conversation process anti-patterns that break in production and how to
fix them.
deprecated: false
hidden: false
metadata:
title: Conversation Process Best Practices | Agent Studio Best Practices
description: >-
Common conversation process mistakes in Agent Studio: async output
references, missing confirmation policies, system\_instructions in output
mappers, and RENDER with Mustache loops. Each pattern with bad example,
explanation, and fix.
robots: index
-------------
Conversation processes wire together slots and actions into multi-turn flows. The patterns here are the ones that pass local testing but break once real users and real data hit them. Each one looks harmless in the editor — the damage shows up in production.
The YAML examples on this page represent conversation process configurations. In practice, you build these in the Agent Studio visual editor — the YAML is the underlying representation of what the editor produces.
Looking for the chained-actions pattern? That's covered in [The Golden Rule](/agent-studio/guides/the-golden-rule) — the single most important architecture principle in Agent Studio.
## Async Output Referenced Later
This is the most common silent failure in conversation processes. It passes every test because timing works differently in the editor.
Only compound actions can run asynchronously — `wait_execution` is a compound action setting. When you use a compound action as an activity inside a conversation process, it defaults to synchronous (`wait_execution: true`). Setting it to `false` makes it fire-and-forget.
### What it looks like
```yaml title="Referencing async output downstream"
# Activity 9: Log analytics (fire-and-forget)
- action:
action_name: log_analytics
wait_execution: false
output_key: analytics_result
# Activity 11: Send notification with the tracking ID
- action:
action_name: send_status_notification
output_mapper:
tracking_id: data.analytics_result.tracking_id # BROKEN
```
### Why it breaks
When `wait_execution: false`:
1. The action is dispatched to the backend
2. Execution continues immediately to the next activity
3. The output is **empty** — the action hasn't completed yet
You can't reference output from an async action because it doesn't exist at the time the next activity runs.
### Real consequence
`data.analytics_result.tracking_id` resolves to `undefined`. The output mapper fails silently or returns `null`. The user sees a broken response with a missing tracking ID — or the entire downstream activity fails with no useful error message.
### The fix
Either make it synchronous if you need the output:
```yaml title="Synchronous execution"
- action:
action_name: log_analytics
wait_execution: true # Wait for completion
output_key: analytics_result
```
Or don't reference the output if you truly want fire-and-forget:
```yaml title="Fire-and-forget (no output reference)"
- action:
action_name: log_analytics
wait_execution: false
# No output_key — nothing to reference later
```
**Rule of thumb:** If any later activity reads `data..*` from a compound action, that compound action must have `wait_execution: true`. Async is only for side effects you never need to read downstream.
***
## No Confirmation on Destructive Actions
The reasoning engine interprets user intent. It does not always interpret it correctly. Without a confirmation step, there's no checkpoint between "engine decided" and "action executed."
### What it looks like
```yaml title="Destructive action without confirmation"
- action:
action_name: delete_user_account
confirmation_policy: false
- action:
action_name: create_calendar_event
confirmation_policy: false
```
### Why it breaks
Users are ambiguous. The reasoning engine resolves ambiguity with its best guess. Without confirmation:
* The wrong item gets deleted
* A meeting is scheduled for the wrong time or wrong people
* There's no chance to catch the error before an irreversible action executes
### Real consequence
User: "Delete my old project"
Engine interprets "old project" as "Current Project" (the most recently discussed one).
Action: Deletes Current Project.
User: "Wait, not that one!"
Too late. Data gone. No undo.
### The fix
```yaml title="Confirmation on destructive actions"
- action:
action_name: delete_user_account
confirmation_policy: true # "Are you sure?"
- action:
action_name: create_calendar_event
confirmation_policy: true # "Does this look right?"
```
**Rule of thumb:** Any action that creates, updates, or deletes data should have `confirmation_policy: true`. The only exception is read-only lookups and logging side effects.
***
## system\_instructions in Output Mapper
This one is tempting. Your action returns a big response payload and you only want to show a few fields to the user. So you ask the LLM to pick them out. The problem is that output mappers are supposed to return structured data, not delegate formatting to an LLM.
### What it looks like
Say your HTTP action returns this payload:
```json title="Sample API response"
{
"ticket": {
"id": "INC-4821",
"status": "open",
"priority": "high",
"assignee": {
"name": "Jane Kim",
"email": "jane.kim@company.com",
"team": "Infrastructure"
},
"created_at": "2026-03-15T14:30:00Z",
"updated_at": "2026-03-16T09:12:00Z",
"sla_breach_at": "2026-03-17T14:30:00Z",
"description": "Production database connection pool exhausted...",
"comments": [...]
}
}
```
You only want the user to see the ticket ID, status, assignee, and SLA deadline. Instead of mapping those fields directly, you use `system_instructions` to ask the LLM to extract them:
```yaml title="LLM call inside output mapper"
output_mapper:
summary:
system_instructions: "Show only the ticket ID, current status, who it's assigned to, and the SLA deadline from the response."
```
### Why it breaks
`system_instructions` triggers an LLM call to generate the value. This is:
1. **Non-deterministic** — different output each time, different field ordering, different phrasing
2. **Slow** — adds an LLM round-trip to every execution
3. **Expensive** — burns tokens on formatting instead of reasoning
4. **Unreliable** — the LLM might hallucinate fields, omit fields, or reformat dates incorrectly
### Real consequence
Sometimes the user sees: "Ticket INC-4821, Status: Open, Assigned to Jane Kim, SLA: March 17"
Sometimes: "Here's a summary: The ticket is open and assigned to Jane from Infrastructure."
Sometimes: The LLM includes the full description you didn't want, or drops the SLA field entirely.
The output is different every time. You can't build reliable downstream logic on non-deterministic data.
### The fix
Map the exact fields you need directly:
```yaml title="Structured output mapper"
output_mapper:
ticket_id: data.ticket.id
status: data.ticket.status
assignee_name: data.ticket.assignee.name
sla_deadline: data.ticket.sla_breach_at
```
The reasoning engine already knows how to present structured data to users conversationally. That's its job — don't duplicate it in the output mapper with an LLM call.
**When is `system_instructions` appropriate in an output mapper?** Only for cosmetic adjustments that don't affect the structural output — adjusting tone, adding formatting like bold or italics, or rephrasing a value for readability. If you're selecting, filtering, or transforming fields from a response, use direct field mapping instead.
***
## RENDER with Mustache Loops
Mustache templates in output mappers seem like a clean way to format lists. In practice, they break in ways that are hard to diagnose.
### What it looks like
```yaml title="Mustache loop in output mapper"
output_mapper:
attendee_display: |
{{#each data.parsed_attendees}}
- {{this.name}}: {{#if this.available}}Available{{else}}Busy{{/if}}
{{/each}}
```
### Why it breaks
1. Mustache rendering is non-deterministic in this context
2. The rendered output is only visible if the **next** activity has `confirmation_policy: true`
3. Complex logic in templates is hard to debug — no error messages, just missing data
### Real consequence
You expect:
```text title="Expected output"
- John: Available
- Jane: Busy
```
You get:
```text title="Actual output"
- : Available
- : Busy
```
Names are missing because context binding failed silently. Or the entire rendered string is empty because the next activity doesn't have confirmation enabled.
### The fix
Return structured data using a [data mapper](/agent-studio/actions/http-actions/data-mapper-in-http-actions):
```yaml title="Structured data with MAP()"
output_mapper:
attendees_with_status:
MAP():
items: data.parsed_attendees
converter:
name: item.name
email: item.email
available: item.email IN data.availability.available
```
Let the reasoning engine format the list for display. It handles internationalization, markdown formatting, and conversational tone — things a Mustache template can't adapt to.
**Data mapper functions** like `MAP()`, `FILTER()`, and `SORT()` handle iteration and transformation without the fragility of template rendering. See the [Data Mapper in HTTP Actions](/agent-studio/actions/http-actions/data-mapper-in-http-actions) reference and the [decision frameworks guide](/agent-studio/guides/decision-frameworks#dsl-vs-python-script-actions) for when to use DSL, data mappers, or Python.
***
## Quick Reference
| Pattern | Severity | Signal | Fix |
| -------------------------------------- | -------- | ------------------------------------------------------------ | --------------------------------------------------------- |
| Async output referenced later | Critical | `wait_execution: false` + `output_key` referenced downstream | Set `wait_execution: true` or don't reference the output |
| No confirmation on destructive actions | High | `confirmation_policy: false` on create/update/delete | Set `confirmation_policy: true` |
| `system_instructions` in output mapper | Medium | `system_instructions` key in any `output_mapper` | Return structured fields, let the reasoning engine format |
| RENDER + Mustache loops | Medium | `{{#each}}` or `{{#if}}` in output mapper values | Use `MAP()`, `FILTER()`, `SORT()` data mapper functions |
***
The single most important CP architecture principle — never chain actions without a slot barrier.
Six slot anti-patterns that break in production: bloated descriptions, wrong types, missing validation.
Six decision trees for action types, slot types, CP vs CA, LLM vs DSL, and data transforms.
When and how to use LLM actions — the right place for system\_instructions.