Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.zavu.dev/llms.txt

Use this file to discover all available pages before exploring further.

Ecommerce assistant

A WhatsApp shopper that does three things really well:
  1. Track orders — “where’s my package?” answered in 1 turn.
  2. Recommend products — “I need a gift for my sister, $50 budget”.
  3. Cart recovery — proactive nudges when someone abandons a cart.
This example mixes two patterns: the agent (reactive — replies to inbound) and a trigger (proactive — fires on inventory events).

Setup

zavu fn init --slug shopper -y
cd shopper

The agent + tools

index.ts
import { defineAgent, defineTool, defineFunction } from "@zavu/functions"
import Zavudev from "@zavudev/sdk"

const zavu = new Zavudev({
  apiKey: process.env.ZAVU_API_KEY!,
  baseURL: process.env.ZAVU_API_BASE_URL,
})

defineAgent({
  senderId: process.env.SENDER_ID!,
  name: "Shop Helper",
  provider: "zavu",
  model: "openai/gpt-4o-mini",
  channels: ["whatsapp"],
  prompt: `Eres el asistente de Acme Shop en WhatsApp.

Tu trabajo:
- Ayudar a los clientes a encontrar productos (call recommend).
- Decirles el estado de sus pedidos (call lookup_order).
- Tomar el pedido si quieren comprar directo desde el chat (call create_order).

Tono:
- Amistoso, breve.
- SIEMPRE incluye el precio cuando recomiendes un producto.
- Si no encuentras lo que piden, ofrece alternativas, nunca digas "no tengo".

Reglas:
- Nunca inventes IDs, precios o stock — usa las tools.
- Si el cliente pregunta por algo no relacionado a la tienda, dile:
  "Para eso te paso con un agente humano" y llama escalate.`,
})

// 1) Order lookup
defineTool({
  name: "lookup_order",
  description:
    "Get the status of a customer's order. Use when they ask about delivery, " +
    "tracking, where their package is.",
  parameters: {
    type: "object",
    properties: {
      orderId: { type: "string", description: "Order number like ACME-12345." },
    },
    required: ["orderId"],
  },
  handler: async ({ orderId }, ctx) => {
    const order = await fetch(
      `${process.env.SHOP_API_URL}/orders/${orderId}`,
      { headers: { authorization: `Bearer ${process.env.SHOP_API_KEY}` } }
    ).then((r) => (r.ok ? r.json() : null))

    if (!order) return { error: "not_found", message: `No encuentro el pedido ${orderId}.` }
    if (order.customerPhone !== ctx?.contactPhone) {
      return { error: "not_yours", message: "Ese pedido está vinculado a otro número." }
    }

    return {
      orderId: order.id,
      status: order.status,                   // "processing" | "shipped" | "out_for_delivery" | "delivered"
      eta: order.estimatedDeliveryAt,
      tracking: order.trackingUrl,
      items: order.items.map((i) => `${i.qty}× ${i.name}`),
      summary:
        order.status === "shipped"
          ? `Tu pedido fue despachado. Llega aprox ${formatDay(order.estimatedDeliveryAt)}. Tracking: ${order.trackingUrl}`
          : order.status === "delivered"
          ? `Ya fue entregado el ${formatDay(order.deliveredAt)}. ¿Algún problema?`
          : `Estado: ${order.status}. ETA: ${formatDay(order.estimatedDeliveryAt)}`,
    }
  },
})

// 2) Product recommendation
defineTool({
  name: "recommend",
  description:
    "Search the catalog and return 3-5 products matching the customer's needs. " +
    "Use when they describe what they want (e.g. 'a gift for my dad under $80, " +
    "he likes camping').",
  parameters: {
    type: "object",
    properties: {
      query: { type: "string", description: "Customer's request, paraphrased." },
      maxPrice: { type: "number", description: "Budget cap in USD." },
      category: { type: "string", description: "Filter by category if obvious." },
    },
    required: ["query"],
  },
  handler: async ({ query, maxPrice, category }) => {
    const res = await fetch(`${process.env.SHOP_API_URL}/search`, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        authorization: `Bearer ${process.env.SHOP_API_KEY}`,
      },
      body: JSON.stringify({
        query,
        filters: {
          ...(maxPrice ? { priceLte: maxPrice } : {}),
          ...(category ? { category } : {}),
          inStock: true,
        },
        limit: 5,
      }),
    }).then((r) => r.json())

    return {
      results: res.products.map((p) => ({
        id: p.id,
        name: p.name,
        price: `$${p.price}`,
        url: `${process.env.SHOP_API_URL}/p/${p.slug}`,
        whyGoodFit: p.relevanceReason,    // your search engine's explanation
      })),
      count: res.products.length,
    }
  },
})

// 3) Create an order from chat
defineTool({
  name: "create_order",
  description:
    "Place an order on behalf of the customer. ONLY call after they explicitly " +
    "confirmed the items and shipping address. Always confirm the total before " +
    "calling.",
  parameters: {
    type: "object",
    properties: {
      items: {
        type: "array",
        items: {
          type: "object",
          properties: {
            productId: { type: "string" },
            quantity: { type: "number" },
          },
          required: ["productId", "quantity"],
        },
      },
      shippingAddress: { type: "string", description: "Full address line." },
      paymentMethod: {
        type: "string",
        enum: ["card_on_file", "cash_on_delivery"],
      },
    },
    required: ["items", "shippingAddress", "paymentMethod"],
  },
  handler: async ({ items, shippingAddress, paymentMethod }, ctx) => {
    const res = await fetch(`${process.env.SHOP_API_URL}/orders`, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        authorization: `Bearer ${process.env.SHOP_API_KEY}`,
      },
      body: JSON.stringify({
        items,
        shippingAddress,
        paymentMethod,
        customerPhone: ctx?.contactPhone,
        source: "whatsapp",
      }),
    }).then((r) => r.json())

    if (res.error) {
      return { error: res.error, message: res.message }
    }

    return {
      orderId: res.id,
      total: `$${res.total}`,
      eta: res.estimatedDeliveryAt,
      summary:
        `Pedido ${res.id} creado por $${res.total}. ` +
        `Envío a "${shippingAddress.slice(0, 40)}…". ` +
        `Llega aprox ${formatDay(res.estimatedDeliveryAt)}.`,
    }
  },
})

// 4) Escalation
defineTool({
  name: "escalate",
  description:
    "Forward this conversation to a human agent. Call when the customer needs " +
    "help outside shopping (returns disputes, account issues, complaints).",
  parameters: {
    type: "object",
    properties: { reason: { type: "string" } },
    required: ["reason"],
  },
  handler: async ({ reason }, ctx) => {
    await fetch(process.env.SLACK_WEBHOOK_URL!, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        text: `:wave: Escalation from ${ctx?.contactPhone}: ${reason}`,
      }),
    })
    return { escalated: true, summary: "Te paso con un agente. Respuesta en ~10 min." }
  },
})

// PROACTIVE: cart recovery trigger.
//
// When your shop backend sends a `cart.abandoned` event to your Zavu function
// (via your own webhook → custom event → trigger fanout), the agent fires off
// a nudge message.
//
// For this to work, you'd:
//   1. Have your shop POST to your Zavu function's HTTP URL (httpEnabled=true)
//      with the abandoned-cart payload.
//   2. Format and send a reminder via the SDK.
export default defineFunction(async (event, ctx) => {
  // Handle non-standard event from your shop: shop posts directly to fn URL.
  // event.body is a stringified JSON body when called via the public HTTP path.
  const body = typeof event.body === "string" ? JSON.parse(event.body) : event
  if (body.type !== "cart.abandoned") return { ok: true }

  const { customerPhone, items, cartUrl, total } = body
  await zavu.messages.send({
    to: customerPhone,
    channel: "whatsapp",
    "Zavu-Sender": process.env.SENDER_ID!,
    text:
      `¡Hola! Vi que dejaste algo en tu carrito 🛒\n\n` +
      items.slice(0, 3).map((i: any) => `• ${i.name}`).join("\n") +
      (items.length > 3 ? `\n• +${items.length - 3} más` : "") +
      `\n\nTotal: $${total}\n${cartUrl}\n\n¿Querés que te ayude a finalizar?`,
  })
  return { ok: true, nudged: true }
})

function formatDay(iso: string) {
  return new Date(iso).toLocaleDateString("es-ES", {
    weekday: "long",
    day: "numeric",
    month: "short",
  })
}
For cart recovery via the function’s public URL, you need httpEnabled: true — but the function is then publicly addressable. Two ways to secure:

Option A: HMAC the cart webhook

Make your shop sign payloads:
import { verifyWebhook } from "@zavu/functions"

export default defineFunction(async (event) => {
  if (typeof event.body !== "string") return { ok: true }
  const sig = event.headers?.["x-shop-signature"]
  if (!verifyWebhook(event.body, sig, process.env.SHOP_WEBHOOK_SECRET!)) {
    return { statusCode: 401, body: '{"error":"invalid_signature"}' }
  }
  // ...
})

Option B: Use Zavu triggers instead

If your shop is also “inside Zavu” (rare for ecommerce, common for messaging), you can use a custom event type. For most shops, Option A is the answer.

Sample conversations

Order tracking

👤 dónde está mi pedido ACME-9421?
🤖 [lookup_order(orderId="ACME-9421")]
🤖 Tu pedido fue despachado. Llega aprox lunes 18 nov.
   Tracking: https://tracker.com/ACME-9421-XYZ
   ¿Algo más?

Discovery → purchase

👤 busco un regalo para mi hermana, le gusta cocinar, hasta $60
🤖 [recommend(query="kitchen gift for sister", maxPrice=60, category="kitchen")]
🤖 Te muestro 3 opciones:

   🥘 Olla de hierro fundido 4Qt — $54 — perfecto para guisos
      https://shop.com/p/olla-4qt
   🔪 Set de 3 cuchillos japoneses — $48 — calidad pro
      https://shop.com/p/knife-set
   📚 Libro "Salt Fat Acid Heat" — $32 — el clásico moderno
      https://shop.com/p/sfah

   ¿Cuál te gusta?

👤 la olla, mándamela a Av Corrientes 1234, Buenos Aires
🤖 ¿Te confirmo el pedido?
   • Olla de hierro fundido 4Qt — $54
   Envío a Av Corrientes 1234, Buenos Aires — gratis
   Total: $54 con tarjeta guardada. ¿Confirmamos?

👤 sí
🤖 [create_order(...)]
🤖 ¡Listo! Pedido ACME-9425 creado por $54.
   Envío a "Av Corrientes 1234, Buenos Aires…".
   Llega aprox jueves 21 nov.

Proactive cart recovery

The shop’s backend posts to https://fn.zavu.dev/<project>/shopper 30 min after a customer abandons checkout. The function sends the nudge:
🤖 ¡Hola! Vi que dejaste algo en tu carrito 🛒
   • Olla de hierro fundido 4Qt
   • Set de 3 cuchillos japoneses
   Total: $102
   https://shop.com/cart/r/abc123
   ¿Querés que te ayude a finalizar?

👤 sí, hacé el pedido
🤖 [recommend ... or directly create_order if confirmed]

Deploy

zavu fn secrets set SENDER_ID jn76vnxet8g5nq661by3v06y1581bmmn
zavu fn secrets set SHOP_API_URL https://shop.acme.com/api
zavu fn secrets set SHOP_API_KEY ...
zavu fn secrets set SHOP_WEBHOOK_SECRET ...
zavu fn secrets set SLACK_WEBHOOK_URL https://hooks.slack.com/...
zavu deploy
If you want cart recovery, also enable HTTP on the function (via dashboard Function detail page → toggle “HTTP enabled”) or pass httpEnabled: true at first init. Note that this exposes the public URL — make sure your HMAC verification is in place.

Metrics worth watching

# Conversion rate: messages sent → orders created
zavu agents executions --sender $SENDER_ID --json | jq '
  group_by(.status) | map({status: .[0].status, count: length})'

# Token cost per conversation (gpt-4o-mini ≈ $0.0002 / 3-turn convo)
zavu agents executions --sender $SENDER_ID --json | jq '
  [.[].cost] | add'

# Cart recovery effectiveness — track which abandoned carts converted
# (your own analytics, since the agent doesn't see this)

Composition with other Zavu primitives

Broadcasts

Black Friday: broadcast a 20% off code to your list. The agent picks up when customers reply with questions.

Templates

For the initial “your order is shipped” notification (outside the 24h window), use a WhatsApp template instead of the agent.

Contacts

Store lastOrderId, lifetimeValue, preferredCategory on contacts. The agent reads them automatically via includeContactMetadata: true.

Sub-accounts

Run separate agents for different storefronts under the same parent Zavu account.

Next

Define tools in depth

JSON schemas, error handling, the LLM’s selection logic.

Triggers

Native event subscriptions without HTTP.