---
title: "Automatic Summarization"
description: "Implement one-click summarization using `generateText` with `Output.object()`. Create concise summaries on demand with structured outputs."
canonical_url: "https://vercel.com/academy/ai-sdk/automatic-summarization"
md_url: "https://vercel.com/academy/ai-sdk/automatic-summarization.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T14:16:18.760Z"
content_type: "lesson"
course: "ai-sdk"
course_title: "Builders Guide to the AI SDK"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Automatic Summarization

# Summarization - Condensing Information Overload

You've classified unstructured data with structured output which is very useful. Now you'll tackle information overload. Long threads, dense articles, and feedback need summarization - a "TL;DR" feature to empower your users and reduce the noise of day-to-day work. You will build a summarization feature with `generateText` and `Output.object()` to condense comments.

## Too Much Text, Too Little Time

We've all been there. You come back to a Slack channel or email thread with dozens of messages. Reading everything takes time you don't have, but you need the gist. Manual summarization is slow and prone to missing key details.

This is a perfect job for AI.

We can feed an entire conversation to the LLM and ask it to pull out the most essential information.

## Setup: The Comment Thread App

\*\*Note: Project Setup\*\*

Continuing with the same codebase from
[Lesson 1.4](./ai-sdk-dev-setup). For this
section, you'll find the summarization example files in the
`app/(3-summarization)/` directory.

Navigate to the `app/(3-summarization)/summarization/` directory in your project.

1. **Run the Dev Server:** If it's not already running, start it: `pnpm dev`
2. **Open the Page:** Navigate to `http://localhost:3000/summarization` in your browser.

You'll see a simple page displaying a list of comments (loaded from `messages.json`). Our task is to make the "Summarize" button functional.

![Screenshot of the '/summarization' page showing the list of comments and the 'Summarize' button.](https://ezs2ytwtdks5l2we.public.blob.vercel-storage.com/ai-sdk-course-summarization-pre-dqpjHr4CQh8tPaXOjzMOc4duno9iy9.png)

## Step 1: Building the Summarization Action

We'll use a Next.js Server Action to handle the AI call.

\*\*Note: What are Server Actions?\*\*

Next.js Server Actions let you run secure server-side code directly from your
React components without manually creating API routes. They're perfect for AI
features because they keep your API keys and sensitive logic on the server
while providing a seamless developer experience for calling backend functions
from the frontend. Learn more about [Server
Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations).

\*\*Note: Server Actions vs API Routes\*\*

Choosing between Server Actions and API routes depends on how the client needs
to talk to your code. Reach for **Server Actions** when the work is triggered by
your App Router UI (forms, buttons, optimistic updates) and you want automatic
cache revalidation and secure access to secrets without exposing an endpoint.
Pick a **Route Handler** when you need a reusable HTTP surface—mobile apps,
third-party integrations, webhooks, or anything the browser will `fetch`—or
when you are building a dedicated backend-for-frontend layer. See the Next.js
docs on [Updating Data](https://nextjs.org/docs/app/getting-started/updating-data)
and [Route Handlers](https://nextjs.org/docs/app/getting-started/route-handlers),
plus the [Backend for Frontend guide](https://nextjs.org/docs/app/guides/backend-for-frontend)
for architecture trade-offs. The AI SDK chat helpers specifically expect a
Route Handler at `/app/api/chat` by default, so keep one in place for
streaming chat flows—even if you also share mutation logic with Server
Actions—learn more in the [AI SDK Next.js App Router quickstart](https://ai-sdk.dev/docs/getting-started/nextjs-app-router).
Start with the option that fits your UI flow, and extract shared mutation
logic into reusable modules when you need both.

1. **Create `` `actions.ts` ``:** Inside the `app/(3-summarization)/summarization/` directory, create a new file named `actions.ts`.

2. **Start with the basic setup:**

```typescript title="app/(3-summarization)/summarization/actions.ts"
'use server';

import { generateText, Output } from 'ai';
import { z } from 'zod';

// TODO: Define the structure for our summary
// Create a Zod schema with these fields:
// - headline (string)
// - context (string)
// - discussionPoints (string)
// - takeaways (string)

export const generateSummary = async (comments: any[]) => {
  console.log('Generating summary for', comments.length, 'comments...');

  // TODO: Use generateText with Output.object() to create the summary
  // - Model: 'openai/gpt-5-mini'
  // - Prompt: Ask to summarize the comments, focusing on key decisions and action items
  // - Output: Output.object({ schema: yourSchema })
  // - Return the generated summary from the 'output' property
};
```

3. **Now implement the schema and `generateText` call:**

```typescript title="app/(3-summarization)/summarization/actions.ts" {6-11, 15-25}
"use server";

import { generateText, Output } from "ai";
import { z } from "zod";

const summarySchema = z.object({
	headline: z.string(),
	context: z.string(),
	discussionPoints: z.string(),
	takeaways: z.string(),
});

export const generateSummary = async (comments: any[]) => {
	console.log("Generating summary for", comments.length, "comments...");
	const { output: summary } = await generateText({
		model: "openai/gpt-5-mini",
		prompt: `Please summarize the following comments concisely, focusing on key decisions and action items.
      Comments:
      ${JSON.stringify(comments)}`,
		output: Output.object({
			schema: summarySchema,
		}),
	});
	console.log("Summary generated:", summary);
	return summary;
};
```

`generateText` with `Output.object()` is versatile. You can use it with a Zod schema anytime you need structured JSON output from an LLM, whether for classification, summarization details, or data extraction.

## Step 2: Wiring Up the Frontend

Let's connect the button in `page.tsx` to our new server action. The file already has basic state set up.

1. **Add the necessary imports at the top of `page.tsx`:**

```typescript title="app/(3-summarization)/summarization/page.tsx"
import { generateSummary } from './actions'; // Import the action
import { SummaryCard } from './summary-card'; // Import the UI component

// Define the expected type based on the action's return type
type Summary = Awaited<ReturnType<typeof generateSummary>>;
```

2. **Add state for the summary (after the existing loading state):**

```typescript title="app/(3-summarization)/summarization/page.tsx" {4}
// ... existing code ...
export default function Home() {
	const [loading, setLoading] = useState(false);
	const [summary, setSummary] = useState<Summary | null>(null);
  // ... existing code ...
```

3. **Replace the button's onClick handler with the actual implementation and add a loading state:**

```typescript title="app/(3-summarization)/summarization/page.tsx" {6-18, 23}
// ... existing code ...
<Button
  variant={"secondary"}
  disabled={loading}
  onClick={async () => {
    setLoading(true);
    setSummary(null); // Clear previous summary
    try {
      // Call the server action
      const result = await generateSummary(messages);
      setSummary(result); // Update state with the result
    } catch (error) {
      // Handle potential errors:
      // - AI might fail schema validation (less likely with good prompts/schemas)
      // - Network issues or API timeouts (especially with very large inputs)
      console.error("Summarization failed:", error);
      // TODO: Add user-friendly error feedback (e.g., toast notification)
    } finally {
      setLoading(false);
    }
  }}
>
  {loading ? "Summarizing..." : "Summarize"}
</Button>
```

1. **Conditionally Render the Summary:** Add the `SummaryCard` component, displaying it only when the `summary` state has data.

```tsx title="app/(3-summarization)/summarization/page.tsx"  {3}
// ... existing code ...
  </div>  
  {summary && <SummaryCard {...summary} />}
  <MessageList messages={messages} />
</main>
```

## Step 3: Run and Observe (Initial Summary)

Check your browser (ensure `pnpm run dev` is active). Click "Summarize".

![Screenshot of the '/summarization' page showing summarized comments](https://ezs2ytwtdks5l2we.public.blob.vercel-storage.com/ai-sdk-course-summarization-complete.png)

The initial summary might work, but it could be verbose or unstructured.

## Step 4: Refining with `.describe()`

Let's improve the summary using Zod's `.describe()` method in `actions.ts` to give the AI more precise instructions.

Update the schema in `actions.ts`:

```typescript title="app/(3-summarization)/summarization/actions.ts" {5, 6-8, 11, 12-14}
// Update the summarySchema with detailed descriptions
const summarySchema = z.object({
  headline: z
    .string()
    .describe('The main topic or title of the summary. Max 5 words.'), // Concise headline
  context: z.string().describe(
    'Briefly explain the situation or background that led to this discussion. Max 2 sentences.', // Length guidance
  ),
  discussionPoints: z
    .string()
    .describe('Summarize the key topics discussed. Max 2 sentences.'), // Focused points
  takeaways: z.string().describe(
    'List the main decisions, action items, or next steps. **Include names** for assigned tasks. Max 2-3 bullet points or sentences.', // Specific instructions!
  ),
});

// ... rest of the generateSummary function ...
```

![Screenshot of the '/summarization' page with enhanced schema](https://ezs2ytwtdks5l2we.public.blob.vercel-storage.com/ai-sdk-course-summarization-enhance-schema.png)

Key changes to the schema code:

- Added `.describe()` to every field.
- Provided specific guidance on length and content focus (e.g., "Include names").

\*\*Note: Performance Note\*\*

Summarizing very long conversations can take time and might hit model context
limits or timeouts. For production apps with extensive text, consider
techniques like chunking the input or using models with larger context
windows.

Save `actions.ts`, refresh the browser page, and click "Summarize" again. The output should now be much cleaner and follow your instructions more closely, especially the takeaways with assigned names!

\*\*Note: 💡 Constraining Summary Output\*\*

Want more control over summary length and format? Try asking an AI assistant:

```markdown title="Prompt: Controlling Summary Length and Structure"
<context>
I'm building an automatic summarization feature using Vercel AI SDK's generateText with Output.object() and Zod schemas.
My current schema has fields: headline, context, discussionPoints, and takeaways.
I'm using `.describe()` to guide the AI, but the output is still too verbose.
</context>

<current-schema>
const summarySchema = z.object({
  headline: z.string().describe('The main topic or title of the summary. Max 5 words.'),
  context: z.string().describe('Briefly explain the situation. Max 2 sentences.'),
  discussionPoints: z.string().describe('Summarize key topics. Max 2 sentences.'),
  takeaways: z.string().describe('List main decisions and action items. Max 2-3 bullet points.')
});
</current-schema>

<problem>
Even with "Max 2 sentences" in the description, the AI is returning:
- Summaries that are 4-5 sentences long
- Bullet points that span multiple lines
- Inconsistent formatting (sometimes paragraphs, sometimes bullets)

How can I enforce stricter length constraints and consistent formatting?
</problem>

<specific-questions>
1. Should I be more explicit in the prompt itself, not just the schema descriptions?
2. Can I use Zod refinements to validate length after generation?
3. Are there better ways to phrase "Max X sentences" that the AI respects more consistently?
4. For bullet points specifically, how do I ensure the AI uses actual markdown bullets vs paragraphs?

Provide concrete code examples showing the techniques that work best.
</specific-questions>
```

This will help you understand advanced techniques for controlling AI output format and length!

## Key Takeaways

- Summarization is a core Invisible AI task for handling information overload
- The AI SDK makes it easy to summarize using `generateText` with `Output.object()` and a schema
- Structured extraction and summarization are powerful together

### Further Reading: Handling Large Inputs for Summarization

Real-world summarization often involves content that exceeds token limits.

- [Recursive Summarization Techniques](https://platform.openai.com/docs/guides/prompt-engineering/strategy-recursively-summarize-or-process-long-documents) — OpenAI's guide to summarizing large content
- [LangChain Summarization Chains](https://python.langchain.com/docs/use_cases/summarization) — Techniques for summarizing long documents using chunking and map-reduce patterns
- [Managing Context Windows and Token Limits](https://platform.openai.com/docs/guides/tokens) — Best practices for working within token limits

\*\*Side Quest: Scale to 1000+ Comments\*\*

\*\*Note: 💡 Need Help with MapReduce Strategy?\*\*

Struggling with how to implement chunking and merging summaries? Try this:

```markdown title="Prompt: Implementing MapReduce for Large-Scale Summarization"
<context>
I'm working on the "Scale to 1000+ Comments" SideQuest in the Vercel AI SDK course.
My current summarization works great for ~50 comments, but I need to handle 1000+ efficiently.
I understand I need to use the MapReduce pattern: chunk → summarize each chunk → merge summaries.
</context>

<current-understanding>
1. Split 1000 comments into chunks of 15-20
2. Use generateText with Output.object() to summarize each chunk in parallel
3. Take all chunk summaries and create a "summary of summaries"
</current-understanding>

<questions>
1. **Chunking:** How do I decide optimal chunk size? Is 15-20 the right number, or should it be based on token count?

2. **Schema evolution:** Should my chunk-level schema be different from my final summary schema?
   - Do I want more detail in chunk summaries (to avoid losing info)?
   - Or should I keep them consistent?

3. **Merging strategy:** When creating the final summary from chunk summaries, how do I:
   - Deduplicate similar points across chunks?
   - Maintain chronological context if comments are a conversation?
   - Aggregate action items without losing attribution?

4. **Parallel processing:** I see the example uses p-limit(5). How do I choose the right concurrency limit?
   - What's the tradeoff between speed and API rate limits?
   - Should I handle rate limit errors with exponential backoff?

5. **Error handling:** If one chunk fails to summarize, should I:
   - Retry that chunk?
   - Skip it and note the gap in the final summary?
   - Fail the entire operation?
</questions>

<specific-implementation-doubt>
For the "Reduce" step, should my prompt be:
"Summarize these chunk summaries into a cohesive final summary"

Or should it be more specific like:
"Merge these chunk summaries, deduplicating common themes and preserving all unique action items"

Recommend an approach with example chunking code and explain trade-offs for each strategy.
</specific-implementation-doubt>
```

**1. Chunking Strategy** - Divide comments into manageable groups:

```typescript title="chunked-summary.ts"
const commentChunks = [];
for (let i = 0; i < comments.length; i += 15) {
  commentChunks.push(comments.slice(i, i + 15));
}
```

**2. MapReduce Pattern** - Summarize chunks, then merge:

```typescript title="map-reduce-summary.ts"
import { generateText, Output } from 'ai';

// Map: Generate individual summaries
const chunkSummaries = await Promise.all(
  commentChunks.map(async (chunk) => {
    const { output } = await generateText({
      model: 'openai/gpt-5-mini',
      prompt: `Summarize these comments: ${JSON.stringify(chunk)}`,
      output: Output.object({ schema: SummarySchema }),
    });
    return output;
  })
);

// Reduce: Create a summary of summaries
const { output: finalSummary } = await generateText({
  model: 'openai/gpt-5-mini',
  prompt: `Create final summary from: ${JSON.stringify(chunkSummaries)}`,
  output: Output.object({ schema: SummarySchema }),
});
```

**3. Progressive Refinement** - Update summary as you process:

```typescript title="progressive-summary.ts"
import { generateText, Output } from 'ai';

let currentSummary = { mainTopics: [], sentiment: "neutral", actionItems: [] };

for (const chunk of commentChunks) {
  const { output: chunkInsights } = await generateText({
    model: 'openai/gpt-5-mini',
    prompt: `
      Update this summary with new comments.
      Current: ${JSON.stringify(currentSummary)}
      New comments: ${JSON.stringify(chunk)}
    `,
    output: Output.object({ schema: SummarySchema }),
  });
  currentSummary = chunkInsights;
}
```

**4. Parallel Processing** - Handle multiple chunks simultaneously:

```typescript title="parallel-summary.ts"
import pLimit from 'p-limit';
const limit = pLimit(5); // Max 5 concurrent API calls

const chunkSummaries = await Promise.all(
  commentChunks.map(chunk => limit(() => summarizeChunk(chunk)))
);
```

**5. Selective Summarization** - Prioritize important comments:

```typescript title="selective-summary.ts"
import { generateText, Output } from 'ai';
import { z } from 'zod';

const { output: classification } = await generateText({
  model: 'openai/gpt-5-mini',
  prompt: `Classify by importance: ${JSON.stringify(comments)}`,
  output: Output.object({
    schema: z.object({
      highPriority: z.array(z.number()),
      mediumPriority: z.array(z.number()),
      lowPriority: z.array(z.number()),
    }),
  }),
});

// Only summarize high and medium priority
const importantComments = [
  ...classification.highPriority.map(i => comments[i]),
  ...classification.mediumPriority.map(i => comments[i])
];
```

## Next up: Precise Data with Structured Extraction

You've classified and summarized so now you're ready to get even more precise by extracting specific details from text using `generateText` with `Output.object()` and refined Zod schemas.

This type of invisible AI starts to make mundane form entry a thing of the past.

In the next lesson, you'll tackle structured extraction for appointments, handling challenges like relative dates with just a few tweaks.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
