Skip to content
Median
Esc
↑↓navigate↵open⌘Jpreview
On this page

Tool examples

Ten tools to copy, each with its risk level and the refusals it returns.

Tool Risk Refuses with
orderStatus low not_signed_in, not_found
subscription low not_signed_in
usageThisMonth low not_signed_in
sendPasswordReset medium not_signed_in
resendInvoice medium not_signed_in, not_found
flagForEngineering medium None
refundSmallOrder reviewed not_signed_in, not_found, already_refunded, over_limit
cancelSubscription high not_signed_in
refundOrder high not_signed_in, not_found, already_refunded
extendTrial high not_signed_in, out_of_range

What each risk level means is in risk levels.

The config file

Every example below is an entry in tools in this file. The two helpers at the top are shared by all of them.

import { defineConfig, p, type ToolContext } from "@mediansh/agent-tools";
import { auth, billing, db, metrics, payments, tracker } from "@/lib/server";

/** Your own id for the customer, or null when your server did not sign them in. */
function customerId(context: ToolContext): string | null {
  return context.visitor.verified ? (context.visitor.externalId ?? null) : null;
}

/** The one refusal shape. Median reads ok: false as the tool saying no. */
function refuse(reason: string, detail?: string) {
  return { ok: false, reason, ...(detail ? { detail } : {}) };
}

export default defineConfig({
  tools: {
    orderStatus: {
      description:
        "Look up one of the signed in customer's orders: where it is and when it should arrive.",
      input: {
        orderNumber: p.string("The order number, like ORD-1042."),
      },
      async execute({ orderNumber }, context) {
        const id = customerId(context);
        if (!id) return refuse("not_signed_in", "Ask the customer to sign in first.");

        const order = await db.orders.findForCustomer(id, orderNumber);
        if (!order) return refuse("not_found", "No order with that number on this account.");

        return {
          status: order.status,
          carrier: order.carrier,
          trackingUrl: order.trackingUrl,
          eta: order.eta,
        };
      },
    },
  },
});

Serve it from one route. See Custom tools.

How the examples behave

  • context.visitor.verified is true only when your server signed the customer’s identity. externalId is only present then. name and email are claims. See Identity.
  • A result with ok: false, known: false, allowed: false, or outcome set to "blocked" or "unknown" is a refusal. The agent reads reason (default tool_refused) and detail, tells the customer what it means, and does not call again. Do not use those fields for data.
  • Any other result is data. The agent answers from it in the customer’s language, so return fields, not sentences.
  • context.toolCallId is unique per call. A second request for the same action gets a new id, so check your own state before a write that must not happen twice. The refund examples do.

What execute receives and may return is in the config reference.

Order status

In the config file above. It reads one order, scoped to the signed in customer. findForCustomer(id, number) cannot return someone else’s order. findByNumber(number) could.

Subscription and plan

subscription: {
  description: "The signed in customer's plan, seat count, and renewal date.",
  async execute(_input, context) {
    const id = customerId(context);
    if (!id) return refuse("not_signed_in", "Ask the customer to sign in first.");

    const account = await db.accounts.findByUserId(id);
    return {
      plan: account.plan,
      seats: { used: account.seatsUsed, included: account.seatsIncluded },
      renewsOn: account.renewsOn,
      status: account.status,
    };
  },
},

A tool with no input takes no arguments. risk defaults to low.

Usage this month

usageThisMonth: {
  description: "How much of their monthly quota the customer has used.",
  async execute(_input, context) {
    const id = customerId(context);
    if (!id) return refuse("not_signed_in", "Ask the customer to sign in first.");

    const usage = await metrics.monthToDate(id);
    return {
      requests: usage.requests,
      included: usage.included,
      overageCost: usage.overageCents / 100,
      resetsOn: usage.resetsOn,
    };
  },
},
sendPasswordReset: {
  description: "Email a fresh password reset link to the address on the account.",
  risk: "medium",
  async execute(_input, context) {
    const id = customerId(context);
    if (!id) return refuse("not_signed_in", "Ask the customer to sign in first.");

    await auth.sendPasswordReset(id, { idempotencyKey: context.toolCallId });
    return { sent: true };
  },
},

medium because a new link invalidates the last one. The agent asks the customer first.

Resend an invoice

resendInvoice: {
  description: "Email a copy of one invoice to the account's billing address.",
  risk: "medium",
  input: {
    invoiceNumber: p.string("The invoice number, like INV-2031."),
  },
  async execute({ invoiceNumber }, context) {
    const id = customerId(context);
    if (!id) return refuse("not_signed_in", "Ask the customer to sign in first.");

    const invoice = await billing.findInvoice(id, invoiceNumber);
    if (!invoice) return refuse("not_found", "No invoice with that number on this account.");

    await billing.email(invoice.id, { idempotencyKey: context.toolCallId });
    return { sent: true, to: invoice.billingEmail };
  },
},

Flag for engineering

flagForEngineering: {
  description: "File a bug report for something in this conversation that looks broken.",
  risk: "medium",
  input: {
    title: p.string("One line, as an engineer would title it."),
    detail: p.string("What breaks, what should happen, and any error text."),
    severity: p.enum(["low", "normal", "urgent"]),
  },
  async execute({ title, detail, severity }, context) {
    const issue = await tracker.createIssue({
      title,
      severity,
      body: `${detail}\n\nMedian conversation: ${context.conversationId}`,
      labels: ["from-support"],
    });
    return { filed: true, reference: issue.key };
  },
},

The conversation id in the issue body links your tracker back to the thread.

Refund a small order

refundSmallOrder: {
  description: "Refund an order of $50 or less in full, back to the original payment method.",
  risk: "reviewed",
  input: {
    orderNumber: p.string("The order number to refund."),
    reason: p.enum(["damaged", "late", "wrong_item"]),
  },
  async execute({ orderNumber, reason }, context) {
    const id = customerId(context);
    if (!id) return refuse("not_signed_in", "Ask the customer to sign in first.");

    const order = await db.orders.findForCustomer(id, orderNumber);
    if (!order) return refuse("not_found", "No order with that number on this account.");
    if (order.refundedAt) return refuse("already_refunded");
    if (order.totalCents > 5000) {
      return refuse("over_limit", "Orders over $50 go through refundOrder.");
    }

    const refund = await payments.refund(order.paymentId, {
      reason,
      approvedBy: context.approvedBy,
      idempotencyKey: context.toolCallId,
    });
    return { refunded: true, amount: refund.amount / 100 };
  },
},

reviewed waits for the customer’s yes, then for the reviewer. The reviewer approves, denies, or passes the call to your team. context.approvedBy is "The reviewer". On a passed call it names the approver, as in Cancel a subscription. Enforce the cap in code as well as in the description. See reviewed calls.

Cancel a subscription

cancelSubscription: {
  description: "Cancel the customer's subscription at the end of the current period.",
  risk: "high",
  input: {
    reason: p
      .enum(["too_expensive", "missing_feature", "switching", "other"])
      .optional(),
  },
  async execute({ reason }, context) {
    const id = customerId(context);
    if (!id) return refuse("not_signed_in", "Ask the customer to sign in first.");

    const result = await billing.cancelAtPeriodEnd(id, {
      reason,
      requestedVia: "support",
      approvedBy: context.approvedBy,
      idempotencyKey: context.toolCallId,
    });
    return { cancelled: true, activeUntil: result.activeUntil };
  },
},

context.approvedBy is the approving teammate’s name, "A teammate" when they have none, or "the API" when a Median key approved it. Store it in your own audit trail.

Refund an order

refundOrder: {
  description: "Refund an order in full, back to the original payment method.",
  risk: "high",
  input: {
    orderNumber: p.string("The order number to refund."),
    reason: p.enum(["damaged", "late", "wrong_item", "goodwill"]),
  },
  async execute({ orderNumber, reason }, context) {
    const id = customerId(context);
    if (!id) return refuse("not_signed_in", "Ask the customer to sign in first.");

    const order = await db.orders.findForCustomer(id, orderNumber);
    if (!order) return refuse("not_found", "No order with that number on this account.");
    if (order.refundedAt) return refuse("already_refunded");

    const refund = await payments.refund(order.paymentId, {
      reason,
      approvedBy: context.approvedBy,
      idempotencyKey: context.toolCallId,
    });
    return { refunded: true, amount: refund.amount / 100 };
  },
},

Each request is decided once. A refund can still run twice if the agent asks again after a request settles, or a teammate runs the tool by hand. The refundedAt check stops both.

Extend a trial

extendTrial: {
  description: "Give the customer more trial days.",
  risk: "high",
  input: {
    days: p.number("How many days to add. Fourteen at most."),
  },
  async execute({ days }, context) {
    const id = customerId(context);
    if (!id) return refuse("not_signed_in", "Ask the customer to sign in first.");
    if (!Number.isInteger(days) || days < 1 || days > 14) {
      return refuse("out_of_range", "Trials can be extended by 1 to 14 days.");
    }

    const trial = await billing.extendTrial(id, days, {
      approvedBy: context.approvedBy,
      idempotencyKey: context.toolCallId,
    });
    return { extended: true, endsOn: trial.endsOn };
  },
},

Check the bounds in code. The description tells the agent the limit, and the refusal’s detail tells it again if it asks for more.

Rules

  • Take the customer from customerId(context), never from an input. A tool that accepts an account id will be handed someone else’s.
  • Refuse with refuse(reason, detail). Return data as fields.
  • Keep documentation, policies and error tables in your knowledge base. See what not to build.

Test each tool against your real endpoint, signed in and not:

median tools test orderStatus --input '{"orderNumber":"ORD-1042"}'
median tools test orderStatus --input '{"orderNumber":"ORD-1042"}' --as user_123

The first should return not_signed_in. Every flag is in the CLI reference.

Was this page helpful?