*** 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.