Decision Frameworks

Six decision trees for picking the right action type, slot type, conversation pattern, and data transformation approach.
View as MarkdownOpen in Claude

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.

Quick Reference

Action TypeUse WhenExample
HTTPStraightforward API call, response maps cleanly to outputGET user profile, create ticket
Script (Python)Need loops, conditionals, error handling, or complex data transforms that DSL/data mappers can’t expressNested filtering with fallbacks, date math across time zones, multi-step string parsing
CompoundOrchestrating 2+ actions in sequence where intermediate results feed the next stepLook up calendar → find room → book meeting
LLMTask requires language understanding — summarization, classification, extraction, generationSummarize ticket thread, classify sentiment
Built-in (mw.*)Platform-native capability — user lookups, approvals, notifications, LLM generationmw.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 run compound actions only. Conversation processes are not an option because there’s no user to interact with.

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 for the handoff pattern.

The Default Conversational Pattern

Most conversational plugins follow the same shape: one conversation process wrapping one compound action.

Default conversational pattern
1# Conversation Process
2activities:
3 - slot_activity:
4 required_slots: [user_input_1, user_input_2]
5 - action_activity:
6 action_name: my_compound_action
7 confirmation_policy: true
8 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:

Two action groups with slot barrier
1activities:
2 - action_activity:
3 action_name: lookup_options # First action group
4 required_slots: [search_query]
5 output_key: options
6
7 - action_activity:
8 action_name: create_resource # Second action group
9 required_slots: [selected_option] # ← Slot barrier
10 confirmation_policy: true
11 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.

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.

Common Mistakes

MistakeWhy It’s WrongFix
string for a person’s nameLLM has to parse “John” into a user ID — unreliableUse User or list[User]
Static resolver for yes/noAdds an unnecessary LLM call to resolve the optionUse boolean
No validation on free textUser can enter anything, action fails downstreamAdd a DSL validation policy
Resolver to populate a dropdown when values are fixedAdds API call overhead for static dataUse 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.

TaskRight ToolWhy
Lowercase a stringDSL: $LOWERCASE(x)Deterministic string operation
Format a dateDSL: $FORMAT_TIME(...)Deterministic transformation
Map status code to labelDSL: LOOKUP()Fixed mapping, no interpretation needed
Concatenate fieldsDSL: $CONCAT([...])Deterministic — never use + for strings
Filter a listDSL: $FILTER(items, fn)Condition-based, no language understanding
Summarize a paragraphLLMRequires language comprehension
Classify a ticketLLMRequires semantic understanding
Extract entities from textLLMRequires NLP
Generate a responseLLMRequires 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 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

CapabilityDSL / Data MappersPython (Script Action)
String operations$LOWERCASE, $UPPERCASE, $TRIM, $CONCAT, $SPLITFull string library
Date/time$TIME, $PARSE_TIME, $FORMAT_TIME, $ADD_DATEdatetime (std lib)
List transforms$MAP, $FILTER, $FIND, MAP(), FILTER()List comprehensions, complex nesting
ConditionalsCONDITIONAL()if/elif/else, complex branching
LookupsLOOKUP()Dict operations, nested lookups
Field extractionresponse.result.data.idDirect dict access
IterationMAP() applies a transform to each item, $MAP for DSLfor, while, generators, early exit with break
Error handlingNo try/catchtry/except, custom error messages
Complex mathBasic arithmetic onlyFull math library
Regex$MATCH(pattern, string) for basic matchingFull 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:

DSL — inline in output mapper
1output_mapper:
2 greeting: $CONCAT(["'Hello,'", data.first_name, data.last_name], "' '")
3 upper_name: data.name.$UPPERCASE()
4 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:

Script action — same result, more overhead
1first = input_args["first_name"]
2last = input_args["last_name"]
3raw_date = input_args["raw_date"]
4
5{
6 "greeting": f"Hello, {first} {last}",
7 "upper_name": input_args["name"].upper(),
8 "formatted_date": raw_date[:10] # assumes ISO format
9}

Example: When Data Mappers Get Strained

Simple data mappers are readable and fast:

Good use of data mappers — simple, readable
1output_mapper:
2 ticket_id: response.result.id
3 status:
4 LOOKUP():
5 key: response.result.state
6 mapping:
7 '1': "'New'"
8 '2': "'In Progress'"
9 '3': "'Resolved'"
10 default: "'Unknown'"

But chaining sort, filter, map, and conditional logic is where the nesting gets deep fast:

Data mappers getting strained — consider Python
1output_mapper:
2 report:
3 MAP():
4 items:
5 SORT():
6 items:
7 FILTER():
8 items: response.records
9 condition: item.active == true
10 key: item.priority
11 converter:
12 title: item.name.$UPPERCASE()
13 severity:
14 CONDITIONAL():
15 condition: item.priority > 3
16 on_pass: "'Critical'"
17 on_fail:
18 CONDITIONAL():
19 condition: item.priority > 1
20 on_pass: "'Medium'"
21 on_fail: "'Low'"
22 owner: $CONCAT([item.assignee.first_name, item.assignee.last_name], "' '")
23 link: $CONCAT(["'https://tickets.internal.com/'", item.id.$TEXT()], "")

A script action is cleaner once you hit this level of nesting:

Script action — same logic, easier to read and debug
1records = input_args["records"]
2
3def severity(p):
4 if p > 3: return "Critical"
5 if p > 1: return "Medium"
6 return "Low"
7
8active = [r for r in records if r.get("active")]
9active.sort(key=lambda r: r["priority"])
10
11report = [
12 {
13 "title": r["name"].upper(),
14 "severity": severity(r["priority"]),
15 "owner": f"{r['assignee']['first_name']} {r['assignee']['last_name']}",
16 "link": f"https://tickets.internal.com/{r['id']}"
17 }
18 for r in active
19]
20{"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 loops, switch conditionals, and 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.

NeedCompound ActionPython (Script Action)
Loop over items and call an action per itemfor with each/in/stepsCan’t call actions from scripts
Branch to different actions based on a conditionswitch with cases/conditionCan’t call actions from scripts
Run independent actions concurrentlyparallel with concurrent stepsSingle-threaded, no parallelism
Complex data transform (regex, math, error handling)No inline scriptingFull Python with try/except, regex, math
Transform data between stepsOutput mappers with DSL/data mappersFull 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:

Compound action — for loop over users
1steps:
2 - action:
3 action_name: mw.batch_get_users_by_email
4 output_key: user_results
5 input_args:
6 user_emails: data.email_list
7 - for:
8 each: user
9 in: data.user_results.user_records
10 output_key: notifications
11 steps:
12 - notify:
13 output_key: notify_result
14 recipient_id: user.user.record_id
15 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:

Compound action — switch on request type
1steps:
2 - switch:
3 cases:
4 - condition: data.request_type == 'access'
5 steps:
6 - action:
7 action_name: submit_access_request
8 output_key: access_result
9 input_args:
10 user_id: data.user_id
11 - condition: data.request_type == 'hardware'
12 steps:
13 - action:
14 action_name: submit_hardware_request
15 output_key: hardware_result
16 input_args:
17 device_type: data.device_type
18 default:
19 steps:
20 - action:
21 action_name: submit_general_request
22 output_key: general_result
23 input_args:
24 description: data.description

When to Use Python Instead

Use a script action when the problem is data manipulation, not action orchestration:

Script action — complex transform that compound actions can't express
1users = input_args["users"]
2active_admins = []
3for user in users:
4 if user.get("status") == "active":
5 roles = user.get("roles", [])
6 if "admin" in roles and user.get("last_login"):
7 try:
8 last_login = parse_date(user["last_login"])
9 if last_login > thirty_days_ago:
10 active_admins.append({
11 "name": user["name"],
12 "email": user["email"],
13 "last_login": format_date(last_login)
14 })
15 except ValueError:
16 continue # Skip malformed dates
17
18{"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

DecisionDefault ChoiceSwitch When
Action typeHTTP actionNeed data transforms → Script. Need sequencing → Compound. Need language → LLM.
CP vs Compound ActionConversational → CP wrapping a CA. Ambient → CA only.System trigger (webhook/schedule) → Compound Action is the only option.
Slot typeFree text stringPerson → User/list[User]. Yes/no → boolean. Display ≠ value → resolver.
LLM vs DSLDSLTask requires language understanding → LLM
Data Mappers & DSL vs PythonDSL/data mappers in mapper3+ nested operations, regex, error handling, complex math → Script action
Compound Actions vs PythonCompound actionNeed inline data manipulation (regex, try/except, complex transforms) → Script action step