Recipe: Order-Status & Product-Support Agent
This recipe builds a single support agent that handles two different kinds of question in the same conversation: general product/policy questions, answered from a knowledge base, and “where’s my order” questions, answered with a live call to an order-lookup API. It combines the RAG pattern from the first cookbook recipe with a router node and a webhook node — this page assumes you’ve read that first recipe and won’t re-explain knowledge base creation or ingestion from scratch.
Every request below needs the x-huat-platform: customer header and a valid dashboard Authorization: Bearer <token> — see Getting Started and Authentication if you haven’t set those up.
Step 1: Create a knowledge base for product/policy questions
Section titled “Step 1: Create a knowledge base for product/policy questions”Same call as Support Agent with a Knowledge Base — create it, then ingest your product catalog and FAQ content with ingestDocument (inline text) or the presigned-upload flow for larger catalog files. Assume this returns knowledge base id 91.
mutation CreateKB { createKnowledgeBase( input: { name: "Product Catalog & FAQ" description: "Product descriptions, sizing, warranty, and returns policy" } ) { id }}Step 2: Create the agent
Section titled “Step 2: Create the agent”mutation CreateOrderSupportAgent { createAgent( input: { name: "Order & Product Support" position: "Support Agent" personaPrompt: "You are a helpful e-commerce support assistant. You can answer questions about products, sizing, warranty, and returns using your reference material, and you can look up the live status of an order when a customer asks. If you don't have information to answer something, say so plainly rather than guessing." useCase: SUPPORT voiceTier: STANDARD } ) { id }}Assume this returns agent id 103.
Step 3: Design the workflow
Section titled “Step 3: Design the workflow”The workflow classifies each message into one of two intents and dispatches accordingly with a router node — the same classify-then-dispatch shape used throughout Worked Examples & Demos, extended here with a real, live data source on one branch instead of a static response.
Flow: start → classify intent (llm) → dispatch (router) → either:
order_statusbranch: extract the order ID (llm,extractFirstInteger) → look up the order (webhook, calls your order API) → reply with the live result (response)product_questionbranch: answer from the knowledge base (llmwithknowledgeBaseId: 91) → reply (response)
…then end_session on either path.
mutation CreateWorkflow($input: CreateWorkflowInput!) { createWorkflow(input: $input) { id }}{ "input": { "name": "Order & Product Support", "description": "Answers product questions from a knowledge base and live order-status questions via webhook." }}Assume this returns workflow id 67.
mutation UpdateWorkflow($input: UpdateWorkflowInput!) { updateWorkflow(input: $input) { id version }}{ "input": { "id": 67, "nodes": [ { "id": "start", "type": "start", "label": "Start", "config": {}, "position": { "x": 0, "y": 0 } }, { "id": "classify_intent", "type": "llm", "label": "Classify Intent", "config": { "outputVar": "intent", "promptTemplate": "Classify this message: \"{{userMessage}}\". Respond with ONLY one word: order_status, or product_question.", "streamOutput": false }, "position": { "x": 0, "y": 100 } }, { "id": "dispatch", "type": "router", "label": "Intent Dispatcher", "config": { "variable": "intent", "defaultTarget": "answer_from_kb" }, "position": { "x": 0, "y": 180 } }, { "id": "extract_order_id", "type": "llm", "label": "Extract Order ID", "config": { "outputVar": "orderId", "extractFirstInteger": true, "promptTemplate": "You are a data extraction step in an automated pipeline, not the assistant talking to the user — you must NOT generate a reply, confirmation, explanation, or any commentary. Extract ONLY the numeric order ID from this message: \"{{userMessage}}\". If no order ID is present, respond with exactly: 0. Respond with ONLY the number, nothing else.", "streamOutput": false }, "position": { "x": -250, "y": 260 } }, { "id": "lookup_order", "type": "webhook", "label": "Check Order Status", "config": { "url": "https://orders.example.com/api/status", "method": "POST", "headers": { "content-type": "application/json" }, "bodyTemplate": "{\"orderId\": {{orderId}}}", "outputVar": "order_status_result", "timeoutMs": 8000 }, "position": { "x": -250, "y": 340 } }, { "id": "respond_order", "type": "response", "label": "Respond With Order Status", "config": { "messageTemplate": "Here's what I found for order #{{orderId}}: {{order_status_result}}", "mood": "helpful" }, "position": { "x": -250, "y": 420 } }, { "id": "answer_from_kb", "type": "llm", "label": "Answer From Product Catalog", "config": { "promptTemplate": "You are a helpful e-commerce support assistant. Answer the customer's question using the provided reference material where relevant: {{userMessage}}", "knowledgeBaseId": 91, "outputVar": "kb_answer" }, "position": { "x": 250, "y": 260 } }, { "id": "respond_kb", "type": "response", "label": "Respond With Product Info", "config": { "messageTemplate": "{{kb_answer}}", "mood": "helpful" }, "position": { "x": 250, "y": 340 } }, { "id": "end", "type": "end_session", "label": "End", "config": { "runEvaluation": true }, "position": { "x": 0, "y": 500 } } ], "edges": [ { "id": "e1", "source": "start", "target": "classify_intent" }, { "id": "e2", "source": "classify_intent", "target": "dispatch" }, { "id": "e3", "source": "dispatch", "target": "extract_order_id", "label": "order_status" }, { "id": "e4", "source": "dispatch", "target": "answer_from_kb", "label": "product_question" }, { "id": "e5", "source": "extract_order_id", "target": "lookup_order" }, { "id": "e6", "source": "lookup_order", "target": "respond_order" }, { "id": "e7", "source": "respond_order", "target": "end" }, { "id": "e8", "source": "answer_from_kb", "target": "respond_kb" }, { "id": "e9", "source": "respond_kb", "target": "end" } ] }}A few choices worth calling out:
dispatch’sdefaultTargetpoints atanswer_from_kb, not a generic fallback response. If the classifier ever outputs something outside the two categories it was given, treating the message as a product question and letting the knowledge base attempt an answer is a more useful default than a dead-end “I didn’t understand” reply — see Router Node for the fulldefaultTarget/circuit-breaker mechanics if you want to harden this further as usage grows.extract_order_idusesextractFirstInteger: trueand is explicitly framed as a pipeline step, not the assistant. This is the same pattern from Worked Examples & Demos — it guaranteeslookup_order’sbodyTemplatereceives a bare number rather than breaking on an off-task reply like “could you clarify the order number?”lookup_orderis awebhooknode, not atoolnode, because this recipe assumes you haven’t registered your order API as an MCP connector yet — a webhook node calls a URL directly with no prior registration step. If you later stand up an MCP server for this API (or already have one), atoolnode is the better long-term fit; the config shape andargsTemplate/bodyTemplatetemplating rules are the same either way.{{orderId}}is unquoted inbodyTemplate({"orderId": {{orderId}}}) becauseextractFirstIntegerguarantees it’s already a bare digit string — quoting it would send a string where the endpoint likely expects an integer. This is only safe because of that guarantee; don’t do this with a raw, unvalidated LLM output.answer_from_kbreuses the exact RAG pattern from the first recipe — sameknowledgeBaseIdfield, same fail-open retrieval behavior if the vector store is briefly unreachable.
Step 4: Publish and attach
Section titled “Step 4: Publish and attach”mutation PublishWorkflow($id: Int!) { publishWorkflow(id: $id) { id isPublished version }}{ "id": 67 }mutation AttachWorkflow { updateAgent(id: "103", input: { workflowId: 67 }) { id workflowId }}As always, confirm isPublished: true before testing — an unpublished workflow runs zero nodes, with no error anywhere. See Agents.
Before you point this at a real order API
Section titled “Before you point this at a real order API”lookup_order.config.url above is a placeholder. Two things to know before pointing it at your real endpoint:
- The URL is checked for SSRF risk at save time, not just execution time — a
urlpointing at a private/internal IP range is rejected byupdateWorkflowbefore the workflow is even stored. This is the same guard applied to action node URLs, and it means your order-lookup API needs to be reachable from the public internet (or through whatever gateway you expose it behind) for this node to call it at all. timeoutMsis clamped to a hard ceiling of 30 seconds, regardless of what you configure. If your real order-lookup endpoint can genuinely take longer than that under load, awebhooknode isn’t the right fit for it as-is — see Webhook Node for the full list of constraints.
What happens when this runs
Section titled “What happens when this runs”A customer sends: “Where’s my order #48213?”
classify_intentreads the message and outputsorder_status.dispatchroutes toextract_order_id, which pulls48213intoorderId(as a string, thanks toextractFirstInteger).lookup_ordercallshttps://orders.example.com/api/statuswith{"orderId": 48213}and waits (blocking, sinceasyncis unset) for the response, storing it inorder_status_result.respond_ordersends the result back: something like “Here’s what I found for order #48213: shipped, arriving Thursday.”
A different customer sends: “Does the men’s jacket run true to size?”
classify_intentoutputsproduct_question.dispatchroutes toanswer_from_kb, which retrieves the relevant sizing-guide chunk from knowledge base91and answers grounded in it.respond_kbsends that answer back verbatim.
Both paths converge on the same end_session node, which queues a post-session evaluation (customer-satisfaction-flavored, since useCase: SUPPORT) you can fetch via the evaluation query — see Evaluation & Export.
Next steps
Section titled “Next steps”- Cookbook: Support Agent with a Knowledge Base (RAG) — the RAG setup this recipe builds on.
- Webhook Node — full config reference, including blocking vs.
async: truefire-and-forget mode. - Router Node — the
defaultTarget/circuit-breaker mechanics for hardening the classifier’s fallback behavior. - Tool Node — the registered-connector alternative to a
webhooknode, once you have an MCP server for your order API. - Troubleshooting — if a webhook call is failing silently or a branch isn’t firing.