Recursive AI

Split a Word document by heading in C#

OneDocument is a free, open-source Word, Excel and PowerPoint document library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

Heading 1 starts each output file.

Terminal
dotnet add package RecursiveAI.OneDocument.Documents

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

C#
using RecursiveAI.OneDocument.Documents;
using var doc = Document.Open("chapters.docx");
using var split = doc.SplitByHeading(1);
int number = 0;
foreach (var piece in split.Pieces)
    piece.Document.SaveAs($"{++number}.docx");

Writes 1.docx and 2.docx; the snippet prints nothing.

Unzip the example, then dotnet run.

Full program

The included chapters.docx has this structure:

Text
Heading 1: Introduction
  Project overview.
  Heading 2: Details
    Schedule and costs.
Heading 1: Delivery
  Ship in June.
C#
using RecursiveAI.OneDocument.Documents;
 
using var doc = Document.Open("chapters.docx");
using var split = doc.SplitByHeading(1);
int number = 0;
foreach (var piece in split.Pieces)
{
    string path = $"{++number}.docx";
    piece.Document.SaveAs(path);
    Console.WriteLine($"{path}: {piece.Title}");
}

dotnet run prints:

Text
1.docx: Introduction
2.docx: Delivery

A heading level outside 1–9 throws ArgumentOutOfRangeException; a document without matching headings returns one piece.

Variations

Include subheadings as split points

Level 2 splits at Heading 1 and Heading 2, not only at Heading 2. This snippet lists the resulting titles without writing files.

C#
using RecursiveAI.OneDocument.Documents;
 
using var doc = Document.Open("chapters.docx");
using var split = doc.SplitByHeading(2);
foreach (var piece in split.Pieces)
    Console.WriteLine(piece.Title);

Output:

Text
Introduction
Details
Delivery

The short answer

Open the DOCX, call SplitByHeading(1), then save each DocumentPiece.Document. Level 1 starts a new piece before each top-level heading. The source stays open separately, and the returned SplitResult owns the piece documents; dispose both doc and split after saving the pieces. The two using var declarations do this automatically.

The key distinction is structure, not appearance. A large bold paragraph is not necessarily a heading. OneDocument looks at outline levels and heading styles, so the document must identify its chapter titles as headings for this operation to find them.

Which heading level should I pass?

Use the deepest heading level that should start a new output file. SplitByHeading(1) keeps a chapter’s subheadings within that chapter. SplitByHeading(2) splits at both Heading 1 and Heading 2. It does not mean “Heading 2 only”. Valid values run from 1 through 9.

A heading is a paragraph whose outline level, set directly or inherited from its style, or built-in heading style identifies it as such. This behavior is documented on the split API. If your chapter titles are only visually formatted, assign heading styles in the source before splitting.

Text before the first matching heading becomes the first piece rather than being discarded. A document without matching headings produces one piece. Keep that case in mind when a job expected many chapters: one output can mean the input has no usable heading markup, not that the save failed.

DocumentPiece.Title gives the heading text. The example uses numbered filenames instead, so headings with punctuation or duplicate names do not become output paths. Each piece also reports FirstBlock and BlockCount in the original body. These are body-block positions, not page numbers or character offsets.

How do I split a Word document into two separate documents?

Heading splitting gives two files when the document has two matching heading ranges, as the sample does. If you need a fixed boundary unrelated to headings, SplitBefore(blockIndices) starts pieces at selected body-block indexes. For section boundaries, SplitBySections() is the structural alternative. These are not equivalent to cutting at a rendered page boundary.

A split piece is made by saving the original in memory and removing the body blocks outside its range. The section covering the end of that range becomes the piece’s final section. Inherited header and footer references are made explicit where needed. Then unused styles, numbering, notes, comments, relationships and unreachable parts are pruned according to the merge and split documentation.

This matters when distributing chapters independently. A piece is a standalone document, not a text excerpt that depends on keeping the original file nearby. However, cross-references to material outside a piece may no longer have a target. Do not treat cutting the body as a guarantee that every reference still makes sense in isolation.

Page-based splitting is a separate operation in the rendering package. It needs layout information and can only start a piece at a body block. A paragraph or table that crosses a page boundary is not cut through its middle by the heading API. Choose heading splitting for chapters and sections of content; choose a layout-based operation only when actual pages are the requirement.

All onedocument recipes