> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# E-commerce shopping agent

> Search the catalog, keep one cart per customer, place the order, and report order status with Products, Baskets, and Orders

This agent sells from the platform's own catalog: it finds products with [`Products`](/reference/sdk/products), keeps one active basket per customer with [`Baskets`](/reference/sdk/baskets), places the order, and reports its status with [`Orders`](/reference/sdk/orders). It needs no external API and no keys. It is six files, and every file compiles against `lua-cli` 3.33.0; run it with the steps on [Running any example](/examples/overview#running-any-example).

*Verified against lua-cli 3.33.0.*

## The conversation

1. The customer describes what they want. The model calls `search_products`, which runs a catalog search and applies an optional price cap.
2. The customer picks a product. `add_to_cart` reuses their active basket or creates one, then adds the item; no basket id passes through the model, because baskets belong to the end user in the conversation.
3. The model reads the items and total back and asks for a shipping address. `checkout` places the order from the active basket.
4. Later, `track_order` reports the status of an order the customer names.

## Primitives and channels

* [Skill and tools](/concepts/skills-and-tools): `shopping`, with `search_products`, `add_to_cart`, `checkout`, and `track_order`.
* Runtime objects: `Products.search`, `Products.getById`, `Baskets.get`, `Baskets.create`, `Baskets.addItem`, `basket.placeOrder`, `Orders.getById`, and the `BasketStatus` enum.
* Channels: any. Baskets and orders are scoped to the end user, so a cart started on the [web widget](/channels/web-widget/quickstart) is the same cart on WhatsApp.

## The code

`Products.search` returns at most five matches in its `products` array; each product's fields are on `data`.

```ts src/skills/tools/SearchProductsTool.ts theme={null}
import { LuaTool, Products } from 'lua-cli';
import { z } from 'zod';

export default class SearchProductsTool implements LuaTool {
  name = 'search_products';
  description = 'Find products in the catalog from a description of what the customer wants';

  inputSchema = z.object({
    query: z.string().describe('What the customer is looking for, for example "running shoes"'),
    maxPrice: z.number().positive().optional().describe('Only return products at or under this price'),
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const { products } = await Products.search(input.query);
    const max = input.maxPrice;
    const matches = max === undefined ? products : products.filter((p) => Number(p.data.price) <= max);

    return {
      products: matches.map((p) => ({
        id: p.data.id,
        name: p.data.name,
        price: p.data.price,
        inStock: p.data.inStock,
      })),
      total: matches.length,
    };
  }
}
```

`Baskets.get(BasketStatus.ACTIVE)` lists the customer's open baskets; a `BasketInstance` exposes `itemCount`, `totalAmount`, and `status` directly and its id through `toJSON()`.

```ts src/skills/tools/AddToCartTool.ts theme={null}
import { LuaTool, Products, Baskets, BasketStatus } from 'lua-cli';
import { z } from 'zod';

export default class AddToCartTool implements LuaTool {
  name = 'add_to_cart';
  description = "Add a product to the customer's cart, creating the cart if they have none";

  inputSchema = z.object({
    productId: z.string().describe('The product id returned by search_products'),
    quantity: z.number().int().min(1).default(1),
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const product = await Products.getById(input.productId);
    if (!product.data.inStock) {
      return { added: false, reason: `${product.data.name} is out of stock` };
    }

    // Baskets belong to the end user in the conversation, so no basket id passes through the model.
    const [open] = await Baskets.get(BasketStatus.ACTIVE);
    const basket = open ?? (await Baskets.create({ currency: 'USD' }));
    const basketId: string = basket.toJSON().id;

    const updated = await Baskets.addItem(basketId, {
      id: product.data.id,
      price: Number(product.data.price),
      quantity: input.quantity,
      SKU: product.data.sku,
    });

    return {
      added: true,
      item: product.data.name,
      itemCount: updated.common.itemCount,
      subtotal: updated.common.totalAmount,
    };
  }
}
```

`basket.placeOrder` converts the active basket into an order; the order's id is `orderId` on the serialized order.

```ts src/skills/tools/CheckoutTool.ts theme={null}
import { LuaTool, Baskets, BasketStatus } from 'lua-cli';
import { z } from 'zod';

export default class CheckoutTool implements LuaTool {
  name = 'checkout';
  description =
    'Place an order for everything in the cart. Read the items and total back to the customer and confirm the address first.';

  inputSchema = z.object({
    shippingAddress: z.object({
      name: z.string(),
      line1: z.string(),
      city: z.string(),
      postalCode: z.string(),
      country: z.string().length(2).describe('ISO 3166-1 alpha-2, for example "US"'),
    }),
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const [basket] = await Baskets.get(BasketStatus.ACTIVE);
    if (!basket || basket.itemCount === 0) {
      return { placed: false, reason: 'The cart is empty' };
    }

    const items = (basket.items ?? []) as Array<{ id: string; price: number; quantity: number }>;
    const order = await basket.placeOrder({ shippingAddress: input.shippingAddress });
    const placed = order.toJSON();

    return {
      placed: true,
      orderId: placed.orderId,
      items: items.map((item) => ({ productId: item.id, quantity: item.quantity })),
      total: basket.totalAmount,
    };
  }
}
```

`Orders.getById` only sees the end user's own orders.

```ts src/skills/tools/TrackOrderTool.ts theme={null}
import { LuaTool, Orders } from 'lua-cli';
import { z } from 'zod';

export default class TrackOrderTool implements LuaTool {
  name = 'track_order';
  description = "Report the status of one of the customer's orders";

  inputSchema = z.object({
    orderId: z.string().describe('The order id returned by checkout'),
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    // Orders are scoped to the end user, so a customer can only read their own.
    const order = await Orders.getById(input.orderId);
    const o = order.toJSON();

    return {
      orderId: input.orderId,
      status: o.status,
      itemCount: o.itemCount,
      total: o.totalAmount,
      placedAt: o.orderDate,
    };
  }
}
```

The skill's `context` makes the model confirm before it calls `checkout`.

```ts src/skills/shopping.skill.ts theme={null}
import { LuaSkill } from 'lua-cli';
import SearchProductsTool from './tools/SearchProductsTool';
import AddToCartTool from './tools/AddToCartTool';
import CheckoutTool from './tools/CheckoutTool';
import TrackOrderTool from './tools/TrackOrderTool';

export default new LuaSkill({
  name: 'shopping',
  description: 'Search the catalog, manage the cart, check out, and track orders',
  context: `Use search_products for anything the customer describes, then add_to_cart with the
product id they choose. Before checkout, read back every item, the quantity, and the total,
and ask for the shipping address. Call checkout only after the customer says yes.
Use track_order when they ask where an order is.`,
  tools: [new SearchProductsTool(), new AddToCartTool(), new CheckoutTool(), new TrackOrderTool()],
});
```

The agent registers the one skill.

```ts src/index.ts theme={null}
import { LuaAgent } from 'lua-cli';
import shoppingSkill from './skills/shopping.skill';

const agent = new LuaAgent({
  name: 'acme-shop',
  persona: `You are the shopping assistant for the Acme store.
Help customers find products, build a cart, and check out. Always state prices and stock,
confirm every item and the total before placing an order, and never place an order the
customer has not explicitly approved.`,
  skills: [shoppingSkill],
});
```

## First run

No variables are needed. Run the search tool; with an empty catalog it returns no products.

```bash theme={null}
lua test --ci skill --name search_products --input '{"query":"lamp"}'
```

```text Output theme={null}
✅ Compiled 6 primitives (1 agent, 1 skill, 4 tools) in 526ms
✅ Selected tool: search_products
Input: {
  "query": "lamp"
}
🚀 Executing tool...
✅ Tool execution successful!

Tool returned: Object — fields: products, total
Output:
{ products: [], total: 0 }
```

Once the catalog has products, walk the whole conversation in the sandbox: `lua chat --ci -e sandbox -m "Find me a desk lamp under $50" -t`, then reuse the printed thread id with `-t <thread-id>` to add the lamp to the cart and check out in the same conversation. Then release it with `lua push all --ci --force`, `lua version create --ci -m "<message>"`, and `lua version promote <n>`; [Release an agent to production](/ship/releasing) explains what each command changes.

## Ways to make it yours

* Load the catalog in the admin dashboard, with `Products.create` from a one-off tool, or by connecting [Shopify](/integrations/shopify) or WooCommerce and letting the store own the cart.
* `checkout` records the order; to take payment, send a payment link from `checkout` or use the [payment component](/channels/formatting/payment).
* Add `Baskets.removeItem` and `Baskets.clear` as tools when customers need to change their mind; both take the basket id from `toJSON()`.
* Send an order-shipped message from a webhook your fulfillment system calls; see [Send proactive messages](/build/send-proactive-messages).

## Next steps

<Columns cols={2}>
  <Card title="Products reference" href="/reference/sdk/products">
    Search, filters, pagination, and the product shape.
  </Card>

  <Card title="Baskets reference" href="/reference/sdk/baskets">
    Statuses, items, metadata, and `placeOrder`.
  </Card>

  <Card title="Web widget quickstart" href="/channels/web-widget/quickstart">
    Put the agent on the store's site.
  </Card>
</Columns>
