logo_smallAxellero.io

Tools

Define auditable tools for HelpdeskAI search, create, retrieve, and log support data.

←Main

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_faq tool demo above.
query {
  faqs {
    items {
      id
      question
      answer
      tags
    }
  }
}

Sample FAQ data (for testing/demo)

idquestionanswertags
1How 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]
2How 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]
3How can I change my phone number?In your profile settings, open the “Contacts” section and update your phone number.[profile phone account]
4Why 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]
5How 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]
6How 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]
7How 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]
8How 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]
9Why 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]
10How 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_relevant node 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 email

    query ($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.
  • prepare_query (JavaScript) — normalize & validate inputs, decide insert vs update

    function 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, department from start.
    • Outputs: { isInsert, input } (normalized payload + flag).
  • 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 (Backbase node) — update existing customer

    mutation (
      $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}}
  • insert (Backbase node) — insert new customer

    mutation (
      $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}}

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

  • start inputs (Agentflow)

    • subject (string, required)
    • description (string)
    • priority (integer)
    • department (string)
    • customer (integer, required)
  • get_data (Backbase node) — fetch customer + priorities

    query ($id: ID!) {
      customers(where: { id: { _eq: $id } }) {
        items {
          id
        }
      }
      priorities {
        items {
          id
          name
        }
      }
    }
    • Variable: id = {{ctx.nodes.start.inputs.customer}}
  • prepare_query (JavaScript) — validate inputs, build payload

    try {
      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}}
  • exception_1 (error handler)

    return ctx.nodes.prepare_query.outputs.data.error;
  • create_ticket (Backbase node) — insert ticket

    mutation (
      $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}}
  • 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, optional author).
  • 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

  • start inputs (Agentflow)

    • ticket (integer, required)
    • messages (array of objects; each should include message and senderType)
  • get_data (Backbase node) — ticket + senders

    query ($id: ID!) {
      tickets(where: { id: { _eq: $id } }) {
        items {
          id
        }
      }
      senders {
        items {
          id
          name
        }
      }
    }
    • Variable: id = {{ctx.nodes.start.inputs.ticket}}
  • checks (JavaScript) — validate ticket exists, capture timestamp

    try {
      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}}
  • 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}}
  • prepare_query (JavaScript, inside loop) — normalize each message

    const 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 message

    mutation (
      $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}}
  • update_ticket (Backbase node) — timestamp the ticket after loop

    mutation {
      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.