How to Build a Reusable AI Classification Subworkflow
Build one agent-mode child workflow in Spojit that classifies inbound message text into structured fields, then call it from any number of parent routing flows so every flow shares the same classification logic.
What This Integration Does
Routing flows tend to repeat the same expensive work: every Front conversation handler, every Slack escalation flow, and every email intake flow needs to read raw message text and decide what it is about, how urgent it is, and where it should go. Copying that AI logic into each workflow means you maintain it in many places, pay for prompt tuning many times, and watch the flows drift apart. This tutorial centralizes the decision into a single Subworkflow that takes a message in and returns a predictable, structured result. Parent flows stay thin: they gather the text, call the child, and act on the fields it returns.
The child workflow runs on a Manual trigger so it can be invoked as a step rather than fired by an event. A parent passes the message text and any metadata as Input; the child runs its own trigger and nodes, classifies the text with a Connector node in Agent mode backed by a Response Schema, and returns that JSON as its final output. The parent pauses while the child runs, then reads the returned fields and routes accordingly. Because changes to a child take effect immediately for every parent, you update the categories or prompt once and all routing flows pick it up on their next run. Re-runs are deterministic in shape: the schema guarantees the same fields every time, even if the wording inside them varies.
Prerequisites
- A Spojit workspace where you can create more than one workflow.
- A Front connection (API key) if a parent flow reads conversations from Front, with access to the inboxes you route.
- A Slack connection (OAuth) if a parent flow posts routed messages to channels.
- An agreed, fixed list of categories and priorities your business uses (for example
billing,technical,sales,complaint,other). The schema enforces these, so decide them before you build. - Familiarity with variables and handlebars templating such as
{{ input.text }}and{{ step.field }}.
Step 1: Create the child classification workflow with a Manual trigger
Create a new workflow named AI Classify Message. Open it in the Workflow Designer and set the Trigger node type to Manual. A Manual trigger means this workflow is invoked as a step by a parent rather than by an external event, and its output is the request body passed in. Plan for the parent to pass an object shaped like this:
{
"text": "Hi, I was charged twice for my May subscription and need a refund urgently.",
"source": "front",
"reference": "cnv_abc123"
}
Inside the child you reference these as {{ input.text }}, {{ input.source }}, and {{ input.reference }}. Keep the input contract small and stable so every parent can satisfy it.
Step 2: Classify the text with a Connector node in Agent mode
Add a Connector node and switch it to Agent mode. Agent mode lets the agent reason over free text and return a judgment, which is exactly what classification needs. In the prompt, hand it the message and tell it precisely what to decide:
Classify the following inbound customer message.
Message:
{{ input.text }}
Choose exactly one category from: billing, technical, sales, complaint, other.
Choose exactly one priority from: low, normal, high, urgent.
Write a one-sentence summary of what the customer wants.
List up to five lowercase topic tags.
Set needsHuman to true only if the message is a complaint or mentions a refund, cancellation, or legal action.
Keep the prompt deterministic in its instructions: name the allowed values explicitly so the model cannot invent new ones. Agent mode costs AI credits, so this single node is the only paid step in the child.
Step 3: Force structured output with a Response Schema
On the same Agent-mode node, open the Response Schema field and define the exact JSON shape you want back. The schema is what makes this child reusable: parents can rely on the same keys every time. Use a schema like:
{
"type": "object",
"properties": {
"category": { "type": "string", "enum": ["billing", "technical", "sales", "complaint", "other"] },
"priority": { "type": "string", "enum": ["low", "normal", "high", "urgent"] },
"summary": { "type": "string" },
"tags": { "type": "array", "items": { "type": "string" } },
"needsHuman": { "type": "boolean" },
"confidence": { "type": "number" }
},
"required": ["category", "priority", "summary", "needsHuman"]
}
Set the node's Output Variable to classification so downstream and the parent can read {{ classification.category }}, {{ classification.priority }}, and the rest. With enum values in place, the returned category and priority are guaranteed to be one of your known values.
Step 4: Shape and return the result
Add a Transform node after the Agent-mode node so the child returns a clean, contract-stable object regardless of any extra fields the model produced. Map only the fields parents need:
{
"category": {{ classification.category }},
"priority": {{ classification.priority }},
"summary": {{ classification.summary }},
"tags": {{ classification.tags }},
"needsHuman": {{ classification.needsHuman }},
"reference": {{ input.reference }}
}
The final node's output becomes the child's return value, which is what the parent's Subworkflow node receives. Echoing reference back lets a parent correlate the result with the record it sent in. Save and run the child once on its own with the Run button, passing a sample body, to confirm the returned object matches the contract.
Step 5: Build a parent routing flow that reads from Front
Create a second workflow, for example Route Front Conversations. Use whichever trigger fits the source: a Schedule trigger that polls on a cron such as */5 * * * * in Australia/Sydney, or a Webhook trigger if Front notifies Spojit. Add a Connector node on the Front connector in Direct mode using the list-conversations tool to gather new conversations, then a second Front Direct-mode node using get-conversation (set its Output Variable to get_conversation) to pull the message body for one. Direct mode is deterministic and spends no AI credits, which keeps the parent cheap. The classification cost lives only in the shared child.
Step 6: Call the subworkflow and route on its fields
Add a Subworkflow node. Set Workflow to AI Classify Message, set its Output Variable to classify, and map Input to the conversation text:
{
"text": {{ get_conversation.text }},
"source": "front",
"reference": {{ get_conversation.id }}
}
The parent pauses while the child runs; its return object arrives as the node's output (for example {{ classify.category }}). Add a Condition node that branches on {{ classify.priority }} equal to urgent or {{ classify.needsHuman }} being true. On the urgent branch, add a Connector node on the Slack connector in Direct mode using send-message to post to your escalation channel with a templated body that includes {{ classify.summary }} and {{ classify.category }}. On the normal branch, route by category, for example a Front add-tag call to label the conversation. Reuse the exact same Subworkflow node in your other parent flows (a Slack intake flow, an email intake flow) by mapping each one's text into the same text input.
Tips
- Keep the child's input contract minimal (
textplus a reference) so any parent can call it without reshaping data. - Put every category and priority in the schema's
enumso routing conditions in parents can compare against known values safely. - When you need to add a category, edit only the child's prompt and Response Schema; every parent inherits the change on its next run.
- Ask Miraxa, the intelligent layer across your automation, to "add a Condition node that checks if
{{ classify.priority }}is urgent and connect the true branch to a Slack send-message node" to scaffold parent branches quickly, then fine-tune in the properties panel.
Common Pitfalls
- Skipping the Response Schema: without it the Agent-mode node returns free text, parents cannot reliably read fields, and routing conditions break.
- Letting categories drift between the prompt and the schema
enum: keep both lists identical or the model may return a value your parents do not handle. - Deep nesting: calling subworkflows that call more subworkflows is allowed but hard to trace. Keep classification one level deep; each child appears as its own execution-history entry.
- Passing empty
text: if a parent's source has no body, the child still runs and pays for an Agent-mode call. Add a Condition in the parent to skip the Subworkflow when{{ get_conversation.text }}is empty.
Testing
Test the child in isolation first: open AI Classify Message, press Run, and pass a handful of representative messages (a billing complaint, a sales question, an empty edge case). Confirm the returned object always has category, priority, summary, and needsHuman, and that the values come from your allowed lists. Then test one parent against a single Front conversation or a single Slack message before widening the trigger scope: check the execution history for both the parent and the child entry, verify the routing branch matched the classification, and only then enable the schedule or webhook for live traffic.