To get an object instead of text from a LangChain agent in NestJS, pass a Zod schema as output to run(). This tutorial turns a customer message into an order your code can use, in a dozen lines.

This is part 3 of AI agents with NestJS. Part 2 gives the agent the catalog tools used here.

Tested with NestJS 12, LangChain 1.x, Zod 4, nestjs-langchain 2.1 and Node 22.

What does structured output change?

Difference between with structured output and without structured output

Same message, same model. Without a schema, the agent answers in text, and your code has to dig the SKU out of a sentence. With one, it answers an object, checked against the schema and typed by it.

The model is still not deterministic: it decides what goes in each field. But the shape of its answer is fixed, and that is what your backend relies on.

How do I describe the answer?

With a Zod object, in orders/order-draft.schema.ts:

import { z } from 'zod'; export const OrderDraft = z.object({ sku: z.string().describe('The SKU of the product in the catalog.'), quantity: z.number().int().positive(), }); export type OrderDraft = z.infer<typeof OrderDraft>; 

The model receives this schema, descriptions included, and fills in each field from them. .transform() and .refine() work too: see docs/structured-output.md.

How do I get it from run()?

Pass the schema as output, in a new orders/orders.controller.ts:

import { Body, Controller, Post } from '@nestjs/common'; import { Agent } from 'nestjs-langchain'; import { OrderDraft } from './order-draft.schema.js'; @Controller('orders') export class OrdersController { constructor(private readonly agent: Agent) {} @Post('draft') async draft(@Body('message') message: string): Promise<OrderDraft> { const { output } = await this.agent.run( `Find the product of this customer message in the catalog, then draft the order.\n\n${message}`, { output: OrderDraft }, ); return output; } } 

Then add OrdersController to the controllers of AppModule.

  • output is an OrderDraft: output.sku compiles, a typo does not.
  • The input asks for the catalog search of part 2: otherwise the model may write a SKU from memory.
  • POST /ask still answers in text: the schema belongs to the run, not to the agent.

Try it

npm run start:dev curl -X POST http://localhost:3000/orders/draft \ -H 'Content-Type: application/json' \ -d '{"message": "Two K2 keyboards, please."}' 
{ "sku": "KB-101", "quantity": 2 } 

The model found the SKU with the searchProducts tool of part 2, then answered through the schema.

Next: test it without an API key

The agent now answers in a shape your code can trust. Part 4 tests all of it with LangChain's fakeModel(), which replays the tool calls and answers you queue: the catalog tools, this endpoint, the retry and the 422, with no provider and no network. The tests are already in the repo, in orders/orders.controller.spec.ts.