Median

Tool examples

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

Updated Oct 1, 20263 minute read
ToolRiskRefuses with
orderStatuslownot_signed_in, not_found
subscriptionlownot_signed_in
usageThisMonthlownot_signed_in
sendPasswordResetmediumnot_signed_in
resendInvoicemediumnot_signed_in, not_found
flagForEngineeringmediumNone
refundSmallOrderreviewednot_signed_in, not_found, already_refunded, over_limit
cancelSubscriptionhighnot_signed_in
refundOrderhighnot_signed_in, not_found, already_refunded
extendTrialhighnot_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.

median.config.ts
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.

:::danger Never take the address as an input. Send to the address on the account. Otherwise anyone can ask for a reset link to be mailed to an address they choose. :::

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.

:::tip The agent can file bugs and feature requests into Signal with no code, and Signal can open them in Linear or GitHub. Write this tool only for a tracker Median does not connect to. See Built-in abilities. :::

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.

Still need help?

    Esc