Categorize and Route
Classify a free-text request into a broad category with an LLM Action, then classify it again using options from that category, like IT then Account Access, or HR then Payroll.
Use this pattern when requests arrive in people’s own words and a keyword ruleset is ineffective or grows too large to maintain. If keywords or another field already decides the category well, skip the LLM and route with switch and DSL (Moveworks’ data-mapping syntax) instead.
Context and problem
Think about a time where you’ve needed to route a request within your organization, or even across your team. VPN and Laptop only make sense to route to IT. PTO and Payroll make sense to route to HR. You could solve this by making one overarching list of options where similar options sit side by side.
But what about when the scenarios blur? A “Software” request could get filed as IT (like requesting access to an existing internal contract) when it’s really a procurement question for finance. We’re likely to see requests get routed incorrectly because of ambiguity and inaccuracy. Splitting the decision into two stages fixes that. The first stage picks the domain. The second stage classifies again, but only against that domain’s own options: the IT classifier routes between otions like VPN and Laptop, the HR classifier routes between options like PTO and Payroll.
This pattern fits problems like:
- Service-desk triage — sort a request into IT, HR, or Finance, then by that domain’s specific services.
- Operations-event triage — sort an incoming alert into a product area, then into its specific failure type.
Solution
Build this as a Compound Action, a sequence of steps that runs on its own from start to finish. It can be triggered by a user reporting a problem, a webhook, or a schedule. If a request doesn’t cleanly match any option, it gets flagged for human review instead of being forced into the wrong bucket.
This pattern uses the following components:
- It first uses the LLM Action
generate_structured_value_actionto classify the broad domain and returns a fixed value from a list that you’ve pre-defined. - It uses a
switchaction to route down the appropriate path (domain) based on the selected value. - The
generate_structured_value_actionLLM Action is then used to categorize the request against the available options for that specific path (domain).
When to use this pattern
Use this pattern when both of the following hold:
- Each top-level category has its own set of subcategories, so stage one determines which smaller label set stage two uses.
- A single classifier over all the options is not accurate enough — usually because there are too many labels, or because the domains blur together (e.g. “Software” issues could plausibly be IT or Finance). Confirm this with a quick before/after check: take 50-100 real past requests, write down what the correct domain and service should have been for each, then run your current single classifier over them and count the misses. Run the same requests through this two-stage version and count its misses too. Only adopt the two-stage version if it makes fewer mistakes than the single classifier.
When not to use this pattern
- One classification is accurate enough. If a single LLM Action against a flat set of options classifies reliably, use it. Don’t add a stage (with another LLM call) you don’t need.
- A fixed rule determines the appropriate domain. If a field value, a keyword, or any other deterministic rule decides the domain for the same input, skip the LLM and pick the domain with
switchand DSL (Moveworks’ data-mapping syntax) instead. See LLM vs DSL.
Example: cross-department service-desk triage
A user reports a problem in their own words, and nothing about its domain or service is known yet. A Compound Action classifies the request, then files it as a properly-routed ticket. This example runs against Purple Suite’s Service Desk Tickets Grid base, so each classified request lands there as a new ticket.
Both classifications (the category and route) include an UNKNOWN option. That way, a request that doesn’t clearly match anything still gets filed and flagged for manual review, instead of forced into the wrong bucket. Steps 3 and 4 show how that fallback plays out.
The overall structure and flow here is tailored to our Purple Suite example. You’ll need to define your own domain categories and service routes, and ultimately, the endpoint that they’ll be sent onto.
We can configure an Input Argument in a Compound Action to receive data into the workflow while making the workflow reusable. In our instance, that would be the problem description that the user has inputted, so we’ll set that as problem_description. We can then refer to that value later in code as data.problem_description. We have some examples that we can try out later.
Step 1: Classify the broad domain
If you’re building this in the Low Code Editor, add this as a structured LLM Action step from the step picker instead of writing YAML. The editor generates the same configuration shown below, so use whichever option you prefer.
This step passes data.problem_description to a structured LLM Action. As we are using the generate_structured_value_action, the LLM is instructed to return data in a specific structure. That is the structure shown in the output_schema block in the below code, meaning the result can be set to IT, HR, Finance, or UNKNOWN. It is a required property and no other properties are allowed, based on the code configuration below. That value is accessible to future actions using the variable data.domain_result.generated_output.domain (Note: domain_result is the name of the output_key in the action, while domain is the name of the property in the output schema).
Step 2: Classify the service, constrained to the chosen domain
We then use switch to route to the domain identified in Step 1. You will notice in the code that we have a case (or a branch) for each domain. This allows us to run a different action depending on the domain. In this example, each domain is configuerd to use the generate_structured_value_action to select a service from a pre-configured list. Put another way, each domain can only classify services in its path.
Notice that in each branch, the output_key has been named service_result. This is important for step 3. It means that whichever path is taken across the domains (or cases), it writes the result to the same data.service_result.generated_output.service, meaning we have a predictable variable that we can use for the rest of our workflow.
Step 3: Create the ticket in Purple Suite
With both classifiers complete, the next step files them as a ticket in PurpleSuite. Unlike Steps 1 and 2, this one starts outside the Compound Action’s YAML. You will need to create an HTTP Action to send the payload to an external system. You can create one by importing the cURL command from PurpleSuite. Name it create_ticket to match the action_name used by the next action in YAML. See HTTP Actions if you need the full walkthrough for setting up a HTTP connector.
TBL-SVC-DESK-TICKETS is the fixed ID of Purple Suite’s Service Desk Tickets Grid table, so you can import this command as-is. The other {{...}} placeholders get filled in from the matching input_args value in the YAML below.
The HTTP action below assumes that we need to pass several inputs, including a title, domain, service and description. Remember that the user provided their problem description as an input to our compound action, so we can access that using data.problem_description. Through this process, we’ve categorized the domain, and we’ve categorized the service, so we can reference those from their respective actions.
One interesting point to observe is that the service field below uses a conditional from DSL. We’re writing an inline IF ... THEN ... ELSE statement, which allows this step to always file a ticket, even when it’s categorized as UNKNOWN (instead of silently dropping a request the classifiers couldn’t confidently place).
Step 4: Return the result
return sends the finished result back out of the Compound Action: the new ticket’s ID, its filed domain and service. But you’ll notice there’s one additional field, in there as well, a needs_review field. The needs_review field is set to true whenever either stage landed on UNKNOWN. This shows that the process couldn’t confidently categorize the ticket, and allows subsequent steps in a process to flag it to a human for review and escalation if required.
Open the created ticket in Purple Suite to see the classification you just ran land as a real record.
Here are some exmaple problem descriptions for inspiration:
Issues and considerations
Now that you’ve seen the pattern built, keep these in mind:
- Constrain each second-stage classifier to its own domain’s options. Give the IT classifier only IT services, the HR classifier only HR services, and so on. That’s what makes two stages more accurate than one flat classifier.
- Always include an
UNKNOWNoption and a default path. Fall back automatically when a safe default exists, or send the request to human review when it genuinely needs a judgment call. A silently dropped request is worse than one flagged for review, as in the example’sUNKNOWN/UNKNOWNticket. - If there’s a live user, ask instead of just flagging. Flagging
UNKNOWNfor review is the right call when nobody’s there to clarify — but that’s the wrong default in a live conversation. If a service classification comes backUNKNOWNand a person is right there, ask a follow-up question (e.g. “Can you provide more information to help us route this to the correct team?”) and re-run that classification with their answer, falling back to human review only if they still can’t clarify. That needs a Conversational Process rather than a Compound Action — see Control Flow for branching on a classification result. - Know where a structured result lives. A structured LLM Action’s output sits at
data.<output_key>.generated_output.<field>, as indata.domain_result.generated_output.domainthroughout this example. - Re-run the before/after check if accuracy looks off once this is live. If you categorize the domain incorrectly, the ticket will never be routed to the right service. A bad domain classifier is worse than no hierarchy at all. Use the check outlined in when to use this pattern.
- Two classifications cost more than one. Each stage is its own LLM call, so this pattern is slower and more expensive per request than a single flat classifier — factor that into whether the accuracy gain above is worth it.
Related guidance
Use generate_structured_value_action when a later step needs a fixed, limited-choice field back — not free text.
Choose between actions inside a Compound Action based on a condition.
Decision Policies and branching in Conversational Processes — for asking a clarifying question instead of flagging for review.
Keep backend decisions in a Compound Action; use a Conversational Process when the decision changes what the user is asked next.