Tools
Define auditable tools for HelpdeskAI search, create, retrieve, and log support data.
Tools
HelpdeskAI relies on a tool-driven architecture: the agent decides, but all system actions run through explicit tools. This section defines the exact tools, their contracts, and how to configure them in Axellero Studio.
What You’ll Configure
- FAQ search with strict link to curated answers
- Customer upsert to avoid duplicate users
- Ticket creation with department/priority routing
- Structured conversation logging (batched messages)
- Ticket retrieval for confirmations and summaries
1. Define tools
Create REST-like tools with strict schemas
2. Enforce contracts
Clear inputs/outputs, no hidden state
3. Test & iterate
Validate responses and edge cases
3. Deploy
Deploy workflows to make commit changes and make them available for calling
Use the Arcade demos to see the exact clicks and payloads. For each tool, copy the node input data from the code blocks to reproduce the demo, or use your own payloads while treating the provided snippets as a reference template.
Tool Inventory (HelpdeskAI)
search_faq— Find the best FAQ entry for a user question.upsert_customer— Create or update a customer record.create_ticket— Open a ticket with department and priority.add_ticket_messages— Batch-add structured conversation summaries to a ticket.
Principles
- Agent orchestrates; tools execute.
- No direct DB access by the agent.
- Every action must be auditable and idempotent where possible.
- Use enums for department/priority; never invent IDs.
- Log structured summaries, not raw chat transcripts.
Tool Specifications
1) search_faq
- When to use: First step for any informational question; try FAQ before escalation.
- Input:
query(string). - Output: Best match with
faq_id,question,answer,confidence. - Rules: If confidence is low, respond transparently and consider escalation.
Arcade demo (prerequisites, build, test, deploy)
Copy-paste: fetch FAQs for search_faq
- Use this GraphQL query in the docs datasource to fetch all FAQs (id/question/answer/tags). It pairs with the
search_faqtool demo above.
query {
faqs {
items {
id
question
answer
tags
}
}
}Sample FAQ data (for testing/demo)
| id | question | answer | tags |
|---|---|---|---|
| 1 | How can I reset my password? | Click “Forgot password” on the login screen, enter your email, and follow the instructions sent to your inbox. | [auth password login] |
| 2 | How can I change my email address? | Go to your profile settings, open the “Personal information” section, and enter a new email address. | [profile email account] |
| 3 | How can I change my phone number? | In your profile settings, open the “Contacts” section and update your phone number. | [profile phone account] |
| 4 | Why was I charged twice? | Double charges may occur due to a technical delay. Usually, one of the transactions is automatically reversed within 24 hours. | [payment billing refund] |
| 5 | How can I get a refund? | To request a refund, please create a support ticket and provide the transaction number and payment date. | [payment refund support] |
| 6 | How can I create a support ticket? | Describe your issue in the chat, and we will automatically create a support ticket for our team. | [support ticket help] |
| 7 | How can I check the status of my ticket? | You can request the ticket status by providing the ticket number in the support chat. | [support ticket status] |
| 8 | How long does it take to process a ticket? | The average ticket processing time is between 1 and 3 business days, depending on the complexity of the issue. | [support sla processing] |
| 9 | Why can’t I log in to my account? | Please check that your email and password are correct. If the issue persists, try resetting your password. | [auth login access] |
| 10 | How can I contact a support agent? | If the automated response does not help, you can create a ticket and a support agent will contact you. | [support operator contact] |
Workflow script: get_relevant node (JavaScript)
- Use this script inside the
get_relevantnode to pick the best FAQ match client-side (when you don’t rely solely on vector search).
function findBestFaqAnswer(faqs, userQuery) {
if (!Array.isArray(faqs)) throw new TypeError("faqs must be an array");
const query = normalizeEn(userQuery);
if (query.length < 2) return null;
const qTokens = uniqueTokensEn(query);
if (qTokens.length === 0) return null;
let best = null;
for (const faq of faqs) {
const question = normalizeEn(faq?.question);
const answer = normalizeEn(faq?.answer);
const tagsText = normalizeEn(tagsToText(faq?.tags));
if (!question && !answer && !tagsText) continue;
let raw = 0;
// Phrase bonus (strong)
if (question.includes(query)) raw += 10;
// Token scoring (question > tags > answer)
for (const t of qTokens) {
if (question.includes(t)) raw += 4;
if (tagsText.includes(t)) raw += 2.5;
if (answer.includes(t)) raw += 1.5;
}
// Coverage bonus: reward matching many tokens, not just one
const matched = countMatchedTokens(qTokens, question, tagsText, answer);
raw += (matched / qTokens.length) * 5;
// Small penalty for accidental hits in long text
const lengthPenalty = Math.min(question.length / 250, 1) * (1 - matched / qTokens.length);
raw -= lengthPenalty * 2;
// Normalize to 0..1
const score = clamp(raw / 20, 0, 1);
if (!best || score > best.score) best = { faq, score };
}
// If too weak, treat as not found -> agent can create a ticket
if (!best || best.score < 0.35) return null;
return {
answer: best.faq.answer ?? "",
faq: best.faq,
score: round(best.score, 2),
};
}
/* ----------------- Helpers (English-only) ----------------- */
function normalizeEn(s) {
return String(s ?? "")
.toLowerCase()
// keep letters/digits; turn punctuation into spaces
.replace(/[^a-z0-9]+/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function uniqueTokensEn(normalized) {
// minimal English stopwords for demo
const stop = new Set([
"the","a","an","and","or","to","of","in","on","for","with","is","are","was","were","be","been",
"it","this","that","these","those","as","at","by","from","into","about","can","could","should",
"i","you","we","they","my","your","our","their","me","us","them","how","what","why","when","where"
]);
const tokens = normalized
.split(" ")
.map(t => t.trim())
.filter(Boolean)
.filter(t => t.length >= 2)
.filter(t => !stop.has(t));
return Array.from(new Set(tokens));
}
function tagsToText(tags) {
if (tags == null) return "";
if (Array.isArray(tags)) return tags.join(" ");
if (typeof tags === "string") return tags; // "auth,password"
if (typeof tags === "object") return JSON.stringify(tags);
return String(tags);
}
function countMatchedTokens(tokens, question, tagsText, answer) {
let c = 0;
for (const t of tokens) {
if (question.includes(t) || tagsText.includes(t) || answer.includes(t)) c++;
}
return c;
}
function clamp(n, min, max) {
return Math.max(min, Math.min(max, n));
}
function round(n, digits = 2) {
const p = 10 ** digits;
return Math.round(n * p) / p;
}
const faqs = ctx.nodes.get_faqs.outputs.data.faqs.items;
const question = ctx.nodes.start.inputs.user_question;
return findBestFaqAnswer(faqs, question);Output mapping (end node)
- Use this mapping in your end node to return the selected FAQ.
{{ctx.nodes.get_relevant.outputs.data.faq}}2) upsert_customer
- When to use: Before creating tickets or logging messages; prevents duplicates.
- Input:
email(required),name, optional profile fields. - Output:
customer_id, normalized customer object. - Rules: Never fabricate emails; if email missing, ask the user to provide it.
Copy-paste snippets for upsert_customer
-
get_customer(Backbase node) — fetch existing customer by emailquery ($email: String!) { customers(where: { email: { _ilike: $email } }) { items { id } } }- Variable:
email = {{ctx.nodes.start.inputs.email}} - Purpose: returns existing
id; empty list means go to insert flow.
- Variable:
-
prepare_query(JavaScript) — normalize & validate inputs, decide insert vs updatefunction normalizeEmail(email) { const e = String(email ?? "").trim().toLowerCase(); console.log(e); if (!e) throw new Error("VALIDATION_ERROR: email is required"); if (!e.includes("@") || e.startsWith("@") || e.endsWith("@")) { throw new Error("VALIDATION_ERROR: invalid email"); } return e; } function normalizeHumanName(name) { if (name === undefined) return undefined; // don't update if (name === null) return ""; // allow clearing return String(name).trim().replace(/\s+/g, " "); } function normalizePhone(phone) { if (phone === undefined) return undefined; // don't update if (phone === null) return null; // explicit null const raw = String(phone).trim(); if (!raw) return null; let out = raw.replace(/[^\d+]/g, ""); if (out.includes("+")) out = "+" + out.replace(/\+/g, ""); const digits = out.replace(/\D/g, ""); if (digits.length < 10) { throw new Error("VALIDATION_ERROR: phone too short"); } return out; } const isInsert = ctx.nodes.get_customer.outputs.data.customers.items.length === 0; const input = {}; input.phone = normalizePhone(ctx.nodes.start.inputs.phone); input.full_name = normalizeHumanName(ctx.nodes.start.inputs.full_name); input.email = normalizeEmail(ctx.nodes.start.inputs.email); input.department = ctx.nodes.start.inputs.department; input.id = ctx.nodes.get_customer.outputs.data.customers.items[0]?.id; return { isInsert, input };- Inputs:
email,full_name,phone,departmentfromstart. - Outputs:
{ isInsert, input }(normalized payload + flag).
- Inputs:
-
branch_1(Branch node) — control flow- Update path condition:
{{!ctx.nodes.prepare_query.outputs.data.isInsert}} - Insert path condition:
{{ctx.nodes.prepare_query.outputs.data.isInsert}}
- Update path condition:
-
update(Backbase node) — update existing customermutation ( $id: ID! $department: Int! $email: String! $full_name: String! $phone: String! ) { update_customers( id: $id input: { department: $department, email: $email, full_name: $full_name, phone: $phone } ) { id } }- Variables:
department = {{ctx.nodes.prepare_query.outputs.data.input.department}}
email = {{ctx.nodes.prepare_query.outputs.data.input.email}}
full_name = {{ctx.nodes.prepare_query.outputs.data.input.full_name}}
phone = {{ctx.nodes.prepare_query.outputs.data.input.phone}}
id = {{ctx.nodes.prepare_query.outputs.data.input.id}}
- Variables:
-
insert(Backbase node) — insert new customermutation ( $department: Int! $email: String! $full_name: String! $phone: String! ) { insert_customers( input: { department: $department, email: $email, full_name: $full_name, phone: $phone } ) { id } }- Variables:
department = {{ctx.nodes.prepare_query.outputs.data.input.department}}
email = {{ctx.nodes.prepare_query.outputs.data.input.email}}
full_name = {{ctx.nodes.prepare_query.outputs.data.input.full_name}}
phone = {{ctx.nodes.prepare_query.outputs.data.input.phone}}
- Variables:
Arcade demo (prerequisites, build, test, deploy)
3) create_ticket
- When to use: After FAQ fails or the user requests escalation.
- Input:
customer_id,title,summary,department(enum),priority(1–3). - Output:
ticket_id,status,created_at. - Rules: Must have
customer_id; confirm with the user before creating.
Arcade demo (prerequisites, build, test, deploy)
Copy-paste snippets for create_ticket
-
startinputs (Agentflow)subject(string, required)description(string)priority(integer)department(string)customer(integer, required)
-
get_data(Backbase node) — fetch customer + prioritiesquery ($id: ID!) { customers(where: { id: { _eq: $id } }) { items { id } } priorities { items { id name } } }- Variable:
id = {{ctx.nodes.start.inputs.customer}}
- Variable:
-
prepare_query(JavaScript) — validate inputs, build payloadtry { const input = {}; const date = new Date().toJSON(); function customerValidation(id) { if (ctx.nodes.get_data.outputs.data.customers.items[0] == null) { throw new Error("VALIDATION_ERROR: customer not found"); } return id; } function priorityValidation(id) { const priorities = ctx.nodes.get_data.outputs.data.priorities.items; if (!priorities.find((el) => el.id === id)) { throw new Error("VALIDATION_ERROR: priority not found"); } return id; } input.channel = 1; input.date = date; input.customer = customerValidation(ctx.nodes.start.inputs.customer); input.description = ctx.nodes.start.inputs.description; input.priority = priorityValidation(ctx.nodes.start.inputs.priority); input.status = 1; input.subject = ctx.nodes.start.inputs.subject; input.department = ctx.nodes.start.inputs.department ?? "GENERAL"; return { success: true, data: input }; } catch (error) { return { success: false, error: String(error?.message ?? error) }; } -
branch_1(Branch node) — control flow- Error path:
{{!ctx.nodes.prepare_query.outputs.data.success}} - Success path:
{{ctx.nodes.prepare_query.outputs.data.success}}
- Error path:
-
exception_1(error handler)return ctx.nodes.prepare_query.outputs.data.error; -
create_ticket(Backbase node) — insert ticketmutation ( $channel: Int! $date: DateTime! $customer: Int! $description: String! $priority: Int! $status: Int! $subject: String! $department: String! ) { insert_tickets( input: { channel: $channel created_at: $date customer: $customer description: $description priority: $priority status: $status subject: $subject updated_at: $date department: $department } ) { id subject description } }- Variables:
channel = {{ctx.nodes.prepare_query.outputs.data.data.channel}}
date = {{ctx.nodes.prepare_query.outputs.data.data.date}}
customer = {{ctx.nodes.prepare_query.outputs.data.data.customer}}
description = {{ctx.nodes.prepare_query.outputs.data.data.description}}
priority = {{ctx.nodes.prepare_query.outputs.data.data.priority}}
status = {{ctx.nodes.prepare_query.outputs.data.data.status}}
subject = {{ctx.nodes.prepare_query.outputs.data.data.subject}}
department = {{ctx.nodes.prepare_query.outputs.data.data.department}}
- Variables:
-
end(outputs){{ctx.nodes.create_ticket.outputs.data.insert_tickets[0]}}
4) add_ticket_messages (batched)
- When to use: Immediately after ticket creation and whenever summarizing progress.
- Input:
ticket_id,messages(array of structured summaries:type,content,timestamp, optionalauthor). - Output: Array of stored message records.
- Rules: Use batches to demonstrate looped logging; no raw chat dumps—only concise summaries (issue, agent summary, technical context).
Arcade demo (prerequisites, build, test, deploy)
Copy-paste snippets for add_ticket_messages
-
startinputs (Agentflow)ticket(integer, required)messages(array of objects; each should includemessageandsenderType)
-
get_data(Backbase node) — ticket + sendersquery ($id: ID!) { tickets(where: { id: { _eq: $id } }) { items { id } } senders { items { id name } } }- Variable:
id = {{ctx.nodes.start.inputs.ticket}}
- Variable:
-
checks(JavaScript) — validate ticket exists, capture timestamptry { if (ctx.nodes.get_data.outputs.data.tickets.items.length === 0) { throw new Error("VALIDATION_ERROR: ticket not found"); } return { success: true, result: new Date().toJSON() }; } catch (error) { return { success: false, error: String(error?.message ?? error) }; } -
branch_1(Branch node) — control flow- Error path:
{{!ctx.nodes.checks.outputs.data.success}} - Success path:
{{ctx.nodes.checks.outputs.data.success}}
- Error path:
-
exception_1(error handler)return ctx.nodes.checks.outputs.data.error; -
loop(Loop node) — iterate messages- Iteration type:
for - Value to iterate:
{{ctx.nodes.start.inputs.messages}}
- Iteration type:
-
prepare_query(JavaScript, inside loop) — normalize each messageconst date = new Date().toJSON(); const item = ctx.nodes.loop.outputs.data.item; const message = item?.message; const sender = item?.senderType; const senderId = ctx.nodes.get_data.outputs.data.senders.items.find((el) => el.name === sender)?.id ?? ctx.nodes.get_data.outputs.data.senders.items.find((el) => el.name === "AGENT")?.id; const input = { message: String(message ?? "").trim(), date, senderId, ticketId: ctx.nodes.start.inputs.ticket, }; return input; -
insert_message(Backbase node) — create ticket messagemutation ( $date: DateTime! $message: String! $senderId: Int! $ticketId: Int! ) { insert_ticket_messages( input: { created_at: $date message: $message sender: $senderId ticket: $ticketId } ) { id } }- Variables:
date = {{ctx.nodes.prepare_query.outputs.data.date}}
message = {{ctx.nodes.prepare_query.outputs.data.message}}
senderId = {{ctx.nodes.prepare_query.outputs.data.senderId}}
ticketId = {{ctx.nodes.prepare_query.outputs.data.ticketId}}
- Variables:
-
update_ticket(Backbase node) — timestamp the ticket after loopmutation { update_tickets( id: {{ctx.nodes.start.inputs.ticket}} input: { updated_at: "{{ctx.nodes.checks.outputs.data.result}}" } ) { id } } -
end(outputs)
Keep tool names, inputs, and outputs exactly as listed. The agent’s system prompt should forbid any action that is not backed by a tool call.