Skip to main content
Glama

escalate_to_vendor

Escalate maintenance tickets to specialized vendors by selecting appropriate trades, updating ticket status, and triggering automated notifications to vendors and tenants.

Instructions

Escalates a maintenance ticket to a specialist vendor. Selects the right vendor from the directory based on trade (plumbing, electrical, hvac, etc.), updates ticket status to 'escalated', triggers an n8n workflow that emails/WhatsApps the vendor, and sends the tenant an update message. Use overrideVendorPhone to assign a specific vendor instead of the default.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
ticketIdYesThe ID of the ticket to escalate.
tradeYesThe trade/specialty required. Used to select the appropriate vendor from the directory.
urgencyNoteYesA clear description of why this is being escalated and any access/safety considerations the vendor needs to know. This is sent directly to the vendor.
preferredTimeNoHuman-readable preferred scheduling window, e.g. 'Tuesday afternoon' or 'ASAP — emergency'. Passed to the vendor via the n8n workflow.
overrideVendorPhoneNoWhatsApp-formatted phone number (e.g. 'whatsapp:+15125550001') to use instead of the default vendor for this trade. Useful for one-off vendor assignments.

Implementation Reference

  • Main handler function that executes the escalate_to_vendor tool logic. It validates the ticket, resolves the vendor based on trade, updates ticket status to 'escalated', triggers n8n webhook for vendor notification, and sends WhatsApp message to tenant.
    export async function handleEscalateToVendor(
      input: EscalateToVendorInput
    ): Promise<EscalateToVendorOutput> {
      const ticket = ticketStore.getById(input.ticketId);
    
      if (!ticket) {
        throw new Error(`Ticket not found: ${input.ticketId}`);
      }
    
      if (ticket.status === "resolved" || ticket.status === "closed") {
        throw new Error(
          `Cannot escalate ticket ${input.ticketId} — it is already "${ticket.status}".`
        );
      }
    
      const vendor = resolveVendor(input.trade as MaintenanceCategory, input.overrideVendorPhone);
    
      // 1. Update ticket status → escalated
      const now = new Date().toISOString();
      const updated = ticketStore.updateStatus(
        input.ticketId,
        "escalated",
        `Escalated to ${vendor.name} (${input.trade}). ${input.urgencyNote}`,
        "Claude MCP Agent"
      );
    
      // Attach vendor to ticket record
      const withVendor = {
        ...updated,
        vendorAssigned: vendor,
        updatedAt: now,
      };
      ticketStore.upsert(withVendor);
    
      // 2. Trigger n8n escalation workflow (vendor notification, CRM logging)
      const n8nResult = await triggerEscalationWebhook({
        ticketId: ticket.id,
        ticketTitle: ticket.title,
        unit: ticket.unit,
        propertyName: ticket.property.name,
        category: ticket.category,
        priority: ticket.priority,
        urgencyNote: input.urgencyNote,
        tenantName: ticket.tenant.name,
        vendorName: vendor.name,
        vendorPhone: vendor.phone,
        vendorEmail: vendor.email,
        preferredTime: input.preferredTime,
      });
    
      if (n8nResult.executionId) {
        ticketStore.appendNote(
          input.ticketId,
          `n8n escalation workflow triggered (executionId=${n8nResult.executionId})`
        );
      }
    
      // 3. Notify tenant that the issue has been escalated to a specialist
      const tenantMessage = buildTenantMessage(
        "update",
        ticket.tenant.name,
        ticket.title,
        `Hi ${ticket.tenant.name.split(" ")[0]}! We've escalated your maintenance request ` +
        `*"${ticket.title}"* to a specialist vendor (${vendor.name}). ` +
        `${input.preferredTime ? `We're targeting ${input.preferredTime} for the visit. ` : ""}` +
        `We'll confirm the exact appointment time shortly.`
      );
    
      const whatsappResult = await sendWhatsApp(ticket.tenant.phone, tenantMessage);
    
      ticketStore.appendNote(
        input.ticketId,
        `[${now}] Tenant notified of escalation via ${whatsappResult.channel}. ` +
        `Vendor: ${vendor.name}`
      );
    
      const finalTicket = ticketStore.getById(input.ticketId)!;
    
      return {
        success: true,
        vendor,
        n8nWorkflowTriggered: n8nResult.triggered,
        n8nExecutionId: n8nResult.executionId,
        whatsappNotification: whatsappResult,
        ticket: finalTicket,
        message:
          `Ticket ${input.ticketId} escalated to ${vendor.name}. ` +
          `n8n workflow ${n8nResult.triggered ? "triggered" : "skipped (not configured)"}. ` +
          `Tenant notified via ${whatsappResult.channel}.`,
      };
    }
  • Zod schema definition for input validation. Defines required fields: ticketId, trade (enum of 7 maintenance categories), urgencyNote (min 10 chars), and optional fields: preferredTime, overrideVendorPhone.
    export const EscalateToVendorSchema = z.object({
      ticketId: z
        .string()
        .describe("The ID of the ticket to escalate."),
      trade: z
        .enum(["plumbing", "electrical", "hvac", "appliance", "structural", "pest", "general"])
        .describe(
          "The trade/specialty required. Used to select the appropriate vendor from the directory."
        ),
      urgencyNote: z
        .string()
        .min(10)
        .describe(
          "A clear description of why this is being escalated and any access/safety considerations " +
          "the vendor needs to know. This is sent directly to the vendor."
        ),
      preferredTime: z
        .string()
        .optional()
        .describe(
          "Human-readable preferred scheduling window, e.g. 'Tuesday afternoon' or 'ASAP — emergency'. " +
          "Passed to the vendor via the n8n workflow."
        ),
      overrideVendorPhone: z
        .string()
        .optional()
        .describe(
          "WhatsApp-formatted phone number (e.g. 'whatsapp:+15125550001') to use instead of the " +
          "default vendor for this trade. Useful for one-off vendor assignments."
        ),
    });
  • Tool registration with MCP server. Registers 'escalate_to_vendor' tool with description, schema, and async handler that calls handleEscalateToVendor and returns JSON result.
    server.tool(
      "escalate_to_vendor",
      "Escalates a maintenance ticket to a specialist vendor. " +
      "Selects the right vendor from the directory based on trade (plumbing, electrical, hvac, etc.), " +
      "updates ticket status to 'escalated', triggers an n8n workflow that emails/WhatsApps the vendor, " +
      "and sends the tenant an update message. " +
      "Use overrideVendorPhone to assign a specific vendor instead of the default.",
      EscalateToVendorSchema.shape,
      async (input) => {
        const result = await handleEscalateToVendor(input);
    
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(result, null, 2),
            },
          ],
        };
      }
    );
  • Helper function that triggers n8n escalation workflow webhook. Sends vendor details, ticket info, and urgency note to n8n automation for vendor email/WhatsApp notification and CRM logging.
    export async function triggerEscalationWebhook(payload: {
      ticketId: string;
      ticketTitle: string;
      unit: string;
      propertyName: string;
      category: string;
      priority: string;
      urgencyNote: string;
      tenantName: string;
      vendorName: string;
      vendorPhone: string;
      vendorEmail: string;
      preferredTime?: string;
    }): Promise<N8nTriggerResult> {
      if (!N8N_ESCALATE) {
        console.warn("[n8n] N8N_WEBHOOK_ESCALATE not set — skipping webhook");
        return { triggered: false, error: "N8N_WEBHOOK_ESCALATE not configured" };
      }
    
      try {
        const response = await http.post(N8N_ESCALATE, payload);
        const executionId: string | undefined =
          response.data?.executionId ?? response.data?.id;
        console.log(`[n8n] Escalation webhook triggered (executionId=${executionId ?? "unknown"})`);
        return { triggered: true, executionId };
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        console.error(`[n8n] Escalation webhook failed: ${msg}`);
        return { triggered: false, error: msg };
      }
    }
  • Helper function that builds tenant notification messages from templates. Used by escalate_to_vendor to construct WhatsApp messages for tenant status updates about escalation.
    export function buildTenantMessage(
      messageType: string,
      tenantName: string,
      ticketTitle: string,
      customMessage?: string,
      scheduledFor?: string
    ): string {
      const firstName = tenantName.split(" ")[0];
    
      if (customMessage) return customMessage;
    
      const formattedTime = scheduledFor
        ? new Date(scheduledFor).toLocaleString("en-US", {
            weekday: "long",
            month: "short",
            day: "numeric",
            hour: "numeric",
            minute: "2-digit",
            hour12: true,
          })
        : "soon";
    
      const templates: Record<string, string> = {
        acknowledgement:
          `Hi ${firstName}! 👋 We've received your maintenance request: *"${ticketTitle}"*. ` +
          `Our team is reviewing it now and will be in touch shortly with next steps. ` +
          `Reply STOP to unsubscribe from updates.`,
    
        scheduled:
          `Hi ${firstName}! Great news — your maintenance request *"${ticketTitle}"* has been scheduled. ` +
          `A technician will visit on *${formattedTime}*. ` +
          `Please ensure someone is home to provide access. Reply if you need to reschedule.`,
    
        update:
          `Hi ${firstName}! Quick update on your request *"${ticketTitle}"*: ` +
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It does well by describing the multi-step process (updates ticket status, triggers workflow, sends messages) and the override capability. However, it doesn't mention potential side effects like whether this action is reversible, permission requirements, rate limits, or error handling for invalid inputs.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized with two sentences that efficiently cover the tool's purpose and key functionality. The first sentence front-loads the main action and process steps, while the second focuses on an important parameter nuance. No wasted words, though it could be slightly more structured.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 5-parameter mutation tool with no annotations and no output schema, the description provides good coverage of the action and process. However, it lacks information about return values, error conditions, or system constraints. Given the complexity of the operation (multiple side effects), more behavioral context would be beneficial.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all 5 parameters thoroughly. The description adds some context about trade selection ('based on trade') and the overrideVendorPhone use case ('assign a specific vendor instead of the default'), but doesn't provide additional semantic meaning beyond what's in the schema descriptions. Baseline 3 is appropriate when schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('escalates a maintenance ticket to a specialist vendor') and distinguishes it from siblings like 'get_open_tickets' (read-only), 'notify_tenant' (notification only), and 'update_maintenance_status' (status update only). It explicitly mentions the multi-step process including vendor selection, status update, workflow triggering, and tenant notification.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context for when to use this tool ('escalates a maintenance ticket to a specialist vendor') and mentions the overrideVendorPhone parameter for specific assignments. However, it doesn't explicitly state when NOT to use it or name alternatives among the sibling tools (e.g., when to use update_maintenance_status instead).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/utkarsh-portfolio/MCPPropTech'

If you have feedback or need assistance with the MCP directory API, please join our Discord server