***
title: Decision Frameworks
position: 1
excerpt: >-
Six decision trees for picking the right action type, slot type, conversation
pattern, and data transformation approach.
deprecated: false
hidden: false
metadata:
title: Decision Frameworks | Agent Studio Best Practices
description: >-
Practical decision trees for Agent Studio: action types, conversation
process vs compound action, slot types, LLM vs DSL, DSL vs Python, and data
mappers vs Python. Stop guessing — follow the tree.
robots: index
-------------
You'll hit the same six decision points over and over when building in Agent Studio. This page gives you a tree for each one. Follow the branches, pick the right tool, move on.
## Action Type Selection
Every plugin needs at least one action. Here's how to pick the right type.
```mermaid
graph TD
A["What does this action need to do?"] --> B{"Call an external API?"}
B -->|Yes| D["**HTTP Action**
Direct API call with
input/output mapping"]
B -->|No| F{"Orchestrate multiple
actions in sequence?"}
F -->|Yes| G["**Compound Action**
Chain steps with data
passing between them"]
F -->|No| H{"Need LLM reasoning
on unstructured text?"}
H -->|Yes| I["**LLM Action**
Summarize, classify,
extract, or generate"]
H -->|No| J{"Need complex data
transforms? (loops,
error handling, regex)"}
J -->|Yes| E["**Script Action (Python)**
Transform data with loops,
conditionals, error handling"]
J -->|No| K{"Is it a platform
capability?"}
K -->|Yes| L["**Built-in Action (mw.\*)**
User lookup, approvals,
notifications, LLM calls"]
K -->|No| D
```
### Quick Reference
| Action Type | Use When | Example |
| -------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| **HTTP** | Straightforward API call, response maps cleanly to output | GET user profile, create ticket |
| **Script (Python)** | Need loops, conditionals, error handling, or complex data transforms that DSL/data mappers can't express | Nested filtering with fallbacks, date math across time zones, multi-step string parsing |
| **Compound** | Orchestrating 2+ actions in sequence where intermediate results feed the next step | Look up calendar → find room → book meeting |
| **LLM** | Task requires language understanding — summarization, classification, extraction, generation | Summarize ticket thread, classify sentiment |
| **Built-in (mw.\*)** | Platform-native capability — user lookups, approvals, notifications, LLM generation | `mw.get_user_by_email`, `mw.create_generic_approval_request`, `mw.generate_text_action` |
Script actions **cannot make HTTP requests** — internet access is blocked at the infrastructure level. Use HTTP actions to call external APIs, then pass the response data into a script action via `input_args` if you need complex transforms on the result.
## Conversation Process vs Compound Action
This decision starts with **how your plugin gets triggered**, not what it does.
* **Conversational trigger** (user sends a message) → you have a user session. Conversation processes are available.
* **System trigger** (webhook or schedule) → no user session. [Ambient agents](/agent-studio/core-concepts/ambient-agents) run compound actions only. Conversation processes are not an option because there's no user to interact with.
```mermaid
graph TD
A["How is this plugin triggered?"] --> B{"User sends a message?
(conversational trigger)"}
B -->|No| C["**Compound Action only**
Ambient agents have no user session.
CPs are not available."]
B -->|Yes| D{"Does the user need to
provide input or confirm
between action groups?"}
D -->|Yes| E["**Conversation Process**
Collects slots, shows results,
gets confirmation between actions.
Can wrap compound actions."]
D -->|No| F["**Conversation Process
wrapping a Compound Action**
CP handles slots + confirmation.
CA handles the integration logic."]
E --> G{"Tempted to chain two
action activities without
a slot in between?"}
G -->|Yes| H["⚠️ **Golden Rule violation**
Move the chain into
a compound action"]
```
A plugin uses **one trigger type**: either conversational triggers or system triggers. If you need both, create two separate plugins. See [connecting ambient agents to conversational agents](/agent-studio/core-concepts/ambient-agents/connecting-ambient-agents-to-conversational-agents) for the handoff pattern.
### The Default Conversational Pattern
Most conversational plugins follow the same shape: **one conversation process wrapping one compound action**.
```yaml title="Default conversational pattern"
# Conversation Process
activities:
- slot_activity:
required_slots: [user_input_1, user_input_2]
- action_activity:
action_name: my_compound_action
confirmation_policy: true
output_key: result
```
The conversation process handles user interaction (slots, confirmation). The compound action handles integration logic (API calls, transforms, sequencing). Clean separation.
### When a Conversation Process Needs Multiple Action Activities
If your conversation process genuinely needs two separate action groups — for example, a lookup followed by a create — separate them with a slot barrier:
```yaml title="Two action groups with slot barrier"
activities:
- action_activity:
action_name: lookup_options # First action group
required_slots: [search_query]
output_key: options
- action_activity:
action_name: create_resource # Second action group
required_slots: [selected_option] # ← Slot barrier
confirmation_policy: true
output_key: created
```
The `required_slots` on the second activity forces the reasoning engine to pause and collect input. That's the slot barrier that satisfies the [Golden Rule](/agent-studio/guides/the-golden-rule).
## Slot Type Selection
Picking the wrong slot type is one of the most common mistakes. It works in testing but breaks in production when real users give unexpected input.
```mermaid
graph TD
A["What data do you
need from the user?"] --> B{"Is it a person
or list of people?"}
B -->|Yes| C["**list[User]** or **User**
Platform resolves identity
from name/email automatically"]
B -->|No| D{"Is it a yes/no
choice?"}
D -->|Yes| E["**boolean**
No resolver needed.
Cheaper than a static
resolver with two options"]
D -->|No| F{"Does the display value
differ from the API value?"}
F -->|Yes| G["**Static Resolver**
Maps display labels to
internal values"]
F -->|No| H{"Need to look up
options from an API?"}
H -->|Yes| I["**Dynamic Resolver**
Fetches options at runtime
via action"]
H -->|No| J["**Free text (string)**
Simple input,
validate with DSL policy"]
```
### Common Mistakes
| Mistake | Why It's Wrong | Fix |
| ----------------------------------------------------- | --------------------------------------------------- | ------------------------------------------ |
| `string` for a person's name | LLM has to parse "John" into a user ID — unreliable | Use `User` or `list[User]` |
| Static resolver for yes/no | Adds an unnecessary LLM call to resolve the option | Use `boolean` |
| No validation on free text | User can enter anything, action fails downstream | Add a DSL validation policy |
| Resolver to populate a dropdown when values are fixed | Adds API call overhead for static data | Use static resolver with hardcoded options |
Every resolver adds an LLM call. A `boolean` slot is resolved without one. If you're building a two-option choice (approve/reject, yes/no, enable/disable), always use `boolean` over a static resolver.
## LLM vs DSL
The fourth commandment: **LLM for language, DSL for logic.** If the task is deterministic — same input always produces same output — use DSL. If it requires understanding natural language, use an LLM action.
| Task | Right Tool | Why |
| ------------------------------ | ------------------------- | ------------------------------------------ |
| Lowercase a string | DSL: `$LOWERCASE(x)` | Deterministic string operation |
| Format a date | DSL: `$FORMAT_TIME(...)` | Deterministic transformation |
| Map status code to label | DSL: `LOOKUP()` | Fixed mapping, no interpretation needed |
| Concatenate fields | DSL: `$CONCAT([...])` | Deterministic — never use `+` for strings |
| Filter a list | DSL: `$FILTER(items, fn)` | Condition-based, no language understanding |
| **Summarize** a paragraph | **LLM** | Requires language comprehension |
| **Classify** a ticket | **LLM** | Requires semantic understanding |
| **Extract** entities from text | **LLM** | Requires NLP |
| **Generate** a response | **LLM** | Requires natural language generation |
### The Test
Ask yourself: *"If I gave this exact input to 100 different people, would they all produce the exact same output?"*
* **Yes** → DSL. It's deterministic.
* **No** → LLM. It requires judgment.
### LLM Action Defaults
When you do use an LLM action:
* Model: `gpt-4o-mini` (fast, cheap, good enough for most tasks)
* Temperature: `0.3` for deterministic tasks (classification, extraction), `0.7` for creative tasks (generation)
* Always set `"additionalProperties": false` in the response schema to prevent hallucinated fields
* See the [LLM Actions reference](/agent-studio/actions/llm-actions) for the full list of available models and configuration options
## Data Mappers & DSL vs Python
DSL functions (`$LOWERCASE`, `$CONCAT`, `$FORMAT_TIME`, etc.) and data mappers (`MAP()`, `FILTER()`, `LOOKUP()`, `CONDITIONAL()`) work together inside output mappers and input mappers. They handle most data transformations without needing a separate action. But they have limits.
### Capability Comparison
| Capability | DSL / Data Mappers | Python (Script Action) |
| ----------------- | -------------------------------------------------------- | --------------------------------------------------- |
| String operations | `$LOWERCASE`, `$UPPERCASE`, `$TRIM`, `$CONCAT`, `$SPLIT` | Full string library |
| Date/time | `$TIME`, `$PARSE_TIME`, `$FORMAT_TIME`, `$ADD_DATE` | `datetime` (std lib) |
| List transforms | `$MAP`, `$FILTER`, `$FIND`, `MAP()`, `FILTER()` | List comprehensions, complex nesting |
| Conditionals | `CONDITIONAL()` | `if/elif/else`, complex branching |
| Lookups | `LOOKUP()` | Dict operations, nested lookups |
| Field extraction | `response.result.data.id` | Direct dict access |
| Iteration | `MAP()` applies a transform to each item, `$MAP` for DSL | `for`, `while`, generators, early exit with `break` |
| Error handling | No try/catch | `try/except`, custom error messages |
| Complex math | Basic arithmetic only | Full `math` library |
| Regex | `$MATCH(pattern, string)` for basic matching | Full `re` module (groups, substitution, lookahead) |
### Decision Rules
1. **Can you express it inline in a mapper?** → Use DSL/data mappers. No extra action, no latency cost.
2. **Is it a simple field extraction, lookup, or list transform?** → Data mappers handle it declaratively.
3. **Nested 3+ data mapper operations, or need regex/error handling/complex math?** → Switch to a script action.
### Example: Inline DSL vs Python Script Action
DSL runs inline in your mapper — no extra action, no latency cost:
```yaml title="DSL — inline in output mapper"
output_mapper:
greeting: $CONCAT(["'Hello,'", data.first_name, data.last_name], "' '")
upper_name: data.name.$UPPERCASE()
formatted_date: $FORMAT_TIME($PARSE_TIME(data.raw_date), '%Y-%m-%d')
```
The same transforms in Python require a separate script action and add latency:
```python title="Script action — same result, more overhead"
first = input_args["first_name"]
last = input_args["last_name"]
raw_date = input_args["raw_date"]
{
"greeting": f"Hello, {first} {last}",
"upper_name": input_args["name"].upper(),
"formatted_date": raw_date[:10] # assumes ISO format
}
```
### Example: When Data Mappers Get Strained
Simple data mappers are readable and fast:
```yaml title="Good use of data mappers — simple, readable"
output_mapper:
ticket_id: response.result.id
status:
LOOKUP():
key: response.result.state
mapping:
'1': "'New'"
'2': "'In Progress'"
'3': "'Resolved'"
default: "'Unknown'"
```
But chaining sort, filter, map, and conditional logic is where the nesting gets deep fast:
```yaml title="Data mappers getting strained — consider Python"
output_mapper:
report:
MAP():
items:
SORT():
items:
FILTER():
items: response.records
condition: item.active == true
key: item.priority
converter:
title: item.name.$UPPERCASE()
severity:
CONDITIONAL():
condition: item.priority > 3
on_pass: "'Critical'"
on_fail:
CONDITIONAL():
condition: item.priority > 1
on_pass: "'Medium'"
on_fail: "'Low'"
owner: $CONCAT([item.assignee.first_name, item.assignee.last_name], "' '")
link: $CONCAT(["'https://tickets.internal.com/'", item.id.$TEXT()], "")
```
A script action is cleaner once you hit this level of nesting:
```python title="Script action — same logic, easier to read and debug"
records = input_args["records"]
def severity(p):
if p > 3: return "Critical"
if p > 1: return "Medium"
return "Low"
active = [r for r in records if r.get("active")]
active.sort(key=lambda r: r["priority"])
report = [
{
"title": r["name"].upper(),
"severity": severity(r["priority"]),
"owner": f"{r['assignee']['first_name']} {r['assignee']['last_name']}",
"link": f"https://tickets.internal.com/{r['id']}"
}
for r in active
]
{"report": report}
```
If your mapper chains `SORT()`, `FILTER()`, `MAP()`, and `CONDITIONAL()` four or five levels deep — stop. Write a script action.
## Compound Actions vs Python
Compound actions have their own control flow: [`for`](/agent-studio/actions/compound-actions/for) loops, [`switch`](/agent-studio/actions/compound-actions/switch) conditionals, and [`parallel`](/agent-studio/actions/compound-actions/parallel) execution. These handle orchestration-level logic — iterating over items, branching between different actions, running steps concurrently. Script actions handle fine-grained data manipulation within a single step.
| Need | Compound Action | Python (Script Action) |
| ---------------------------------------------------- | ------------------------------------ | ---------------------------------------- |
| Loop over items and call an action per item | `for` with `each`/`in`/`steps` | Can't call actions from scripts |
| Branch to different actions based on a condition | `switch` with `cases`/`condition` | Can't call actions from scripts |
| Run independent actions concurrently | `parallel` with concurrent steps | Single-threaded, no parallelism |
| Complex data transform (regex, math, error handling) | No inline scripting | Full Python with try/except, regex, math |
| Transform data between steps | Output mappers with DSL/data mappers | Full Python in a script step |
### The Key Distinction
* **Compound actions** orchestrate — they decide *which actions run* and *in what order*.
* **Script actions** transform — they manipulate *data within a single step*.
You'll often use both: a compound action orchestrates the flow, and a script action handles a tricky transform at one step.
### Example: Compound Action `for` Loop
Send a notification to each user in a list:
```yaml title="Compound action — for loop over users"
steps:
- action:
action_name: mw.batch_get_users_by_email
output_key: user_results
input_args:
user_emails: data.email_list
- for:
each: user
in: data.user_results.user_records
output_key: notifications
steps:
- notify:
output_key: notify_result
recipient_id: user.user.record_id
message: data.message_text
```
You can't do this in a script action — scripts can't call other actions. This is compound action territory.
### Example: Compound Action `switch`
Route to different actions based on a condition:
```yaml title="Compound action — switch on request type"
steps:
- switch:
cases:
- condition: data.request_type == 'access'
steps:
- action:
action_name: submit_access_request
output_key: access_result
input_args:
user_id: data.user_id
- condition: data.request_type == 'hardware'
steps:
- action:
action_name: submit_hardware_request
output_key: hardware_result
input_args:
device_type: data.device_type
default:
steps:
- action:
action_name: submit_general_request
output_key: general_result
input_args:
description: data.description
```
### When to Use Python Instead
Use a script action when the problem is **data manipulation**, not action orchestration:
```python title="Script action — complex transform that compound actions can't express"
users = input_args["users"]
active_admins = []
for user in users:
if user.get("status") == "active":
roles = user.get("roles", [])
if "admin" in roles and user.get("last_login"):
try:
last_login = parse_date(user["last_login"])
if last_login > thirty_days_ago:
active_admins.append({
"name": user["name"],
"email": user["email"],
"last_login": format_date(last_login)
})
except ValueError:
continue # Skip malformed dates
{"active_admins": active_admins}
```
This needs try/except, date parsing, and conditional filtering within a single list — that's a script action's job. A compound action `for` loop can iterate and call actions, but it can't do inline error handling or complex transforms.
***
## Quick Reference: All Six Decisions
| Decision | Default Choice | Switch When |
| ---------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Action type | HTTP action | Need data transforms → Script. Need sequencing → Compound. Need language → LLM. |
| CP vs Compound Action | Conversational → CP wrapping a CA. Ambient → CA only. | System trigger (webhook/schedule) → Compound Action is the only option. |
| Slot type | Free text `string` | Person → `User`/`list[User]`. Yes/no → `boolean`. Display ≠ value → resolver. |
| LLM vs DSL | DSL | Task requires language understanding → LLM |
| Data Mappers & DSL vs Python | DSL/data mappers in mapper | 3+ nested operations, regex, error handling, complex math → Script action |
| Compound Actions vs Python | Compound action | Need inline data manipulation (regex, try/except, complex transforms) → Script action step |
The single most important architecture principle — never chain actions without a slot barrier.
Full reference for compound actions: steps, return, for loops, switch, parallel, and output mapping.
Deeper comparison with examples and the full decision process.
How to configure LLM actions for summarization, classification, extraction, and generation.