COBOL WRITE a report file: headers, detail lines, page breaks
A GnuCOBOL program writes a paginated report: page header with the run date, column headings, edited detail lines, a page total, and a break every 23 lines.
A batch program that reads a file usually ends by printing one, and printing in COBOL means writing lines to a file with the WRITE statement's ADVANCING phrase. This tutorial builds a paginated transaction register: a header on every page carrying the run date and the page number, column headings, one edited detail line per record, a control total at the foot of each page, and a summary page of grand totals. The break comes every 23 detail lines, and the article walks the exact record that triggers the first one. It reads its input the way reading a sequential file to end of file does, and every amount on the page goes through one of the pictures from the edited pictures lesson.
The listing was desk-checked against the GnuCOBOL Programmer's Guide and the GnuCOBOL Manual; cobc is not installed on the machine this was written on, so nothing here claims a compile. The report arithmetic is a different matter: the Python script shipped alongside builds the same report with the same page rules and the same edited pictures, and every number quoted below came out of running it.
The program
>>SOURCE FORMAT IS FREE
*> cobol-write-report.cob — read a fixed-width
*> transaction file and write a paginated report
*> Dialect: GnuCOBOL 3.x, free format
*> Status : desk-checked; NOT compiled (no cobc on this machine)
IDENTIFICATION DIVISION.
PROGRAM-ID. WRITE-REGISTER.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT Txn-File
ASSIGN TO "datasets/cobol-write-report-file-page-breaks.dat"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-Txn-Status.
SELECT Report-File
ASSIGN TO "register.txt"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-Rpt-Status.
DATA DIVISION.
FILE SECTION.
FD Txn-File.
COPY "cobol-write-report-file-page-breaks.cpy".
*> The report record is one print line. Nothing is described inside it:
*> every line is built in WORKING-STORAGE and written with FROM.
FD Report-File.
01 Report-Line PIC X(72).
WORKING-STORAGE SECTION.
77 WS-Txn-Status PIC XX.
77 WS-Rpt-Status PIC XX.
01 WS-Flags.
05 WS-EOF-Flag PIC X VALUE "N".
88 End-Of-File VALUE "Y".
05 WS-Page-Kind PIC X VALUE "D".
88 Summary-Page VALUE "S".
*> Page geometry. Change these and the whole report re-paginates:
*> 30 usable lines = 5 header + 23 detail + 2 footer.
77 WS-Page-Size PIC 9(3) VALUE 30.
77 WS-Header-Lines PIC 9(3) VALUE 5.
77 WS-Footer-Lines PIC 9(3) VALUE 2.
77 WS-Summary-Lines PIC 9(3) VALUE 7.
*> The hand-kept line counter: how many lines this page already holds.
77 WS-Page-Line PIC 9(3) VALUE 0.
77 WS-Page-No PIC 9(3) VALUE 0.
77 WS-Wanted PIC 9(3) VALUE 0.
77 WS-Needed PIC 9(3) VALUE 0.
77 WS-Read-Count PIC 9(5) VALUE 0.
77 WS-Detail-Count PIC 9(5) VALUE 0.
77 WS-Page-Total PIC 9(9)V99 VALUE 0.
77 WS-Debit-Total PIC 9(9)V99 VALUE 0.
77 WS-Credit-Total PIC 9(9)V99 VALUE 0.
77 WS-Net PIC S9(9)V99 VALUE 0.
77 WS-Show-Count PIC ZZ,ZZ9.
*> FUNCTION CURRENT-DATE returns 21 characters: YYYYMMDDhhmmsshh then the
*> sign and size of the offset from Greenwich. Only the first 14 are used.
01 WS-Now.
05 WS-Now-Ymd PIC 9(8).
05 WS-Now-Hh PIC 9(2).
05 WS-Now-Mi PIC 9(2).
05 WS-Now-Ss PIC 9(2).
05 FILLER PIC X(7).
01 WS-Head-1.
05 FILLER PIC X(2) VALUE SPACES.
05 FILLER PIC X(26) VALUE "DAILY TRANSACTION REGISTER".
05 FILLER PIC X(28) VALUE SPACES.
05 FILLER PIC X(5) VALUE "PAGE ".
05 HD-Page PIC ZZ9.
05 FILLER PIC X(8) VALUE SPACES.
01 WS-Head-2.
05 FILLER PIC X(2) VALUE SPACES.
05 FILLER PIC X(4) VALUE "RUN ".
05 HD-Date PIC 9(4)/9(2)/9(2).
05 FILLER PIC X(2) VALUE SPACES.
05 HD-Hour PIC 9(2).
05 FILLER PIC X VALUE ":".
05 HD-Minute PIC 9(2).
05 FILLER PIC X VALUE ":".
05 HD-Second PIC 9(2).
05 FILLER PIC X(46) VALUE SPACES.
01 WS-Head-Cols.
05 FILLER PIC X(2) VALUE SPACES.
05 FILLER PIC X(6) VALUE "REF".
05 FILLER PIC X(2) VALUE SPACES.
05 FILLER PIC X(10) VALUE "DATE".
05 FILLER PIC X(2) VALUE SPACES.
05 FILLER PIC X(3) VALUE "BR".
05 FILLER PIC X(2) VALUE SPACES.
05 FILLER PIC X(28) VALUE "DESCRIPTION".
05 FILLER PIC X(2) VALUE SPACES.
05 FILLER PIC X(12) VALUE " AMOUNT".
05 FILLER PIC X(3) VALUE " T ".
01 WS-Head-Summary.
05 FILLER PIC X(2) VALUE SPACES.
05 FILLER PIC X(70) VALUE "REPORT SUMMARY".
01 WS-Blank-Line PIC X(72) VALUE SPACES.
01 WS-Rule-Line PIC X(72) VALUE ALL "-".
01 WS-Detail-Line.
05 FILLER PIC X(2) VALUE SPACES.
05 DL-Ref PIC X(6).
05 FILLER PIC X(2) VALUE SPACES.
05 DL-Date PIC 9(4)/9(2)/9(2).
05 FILLER PIC X(2) VALUE SPACES.
05 DL-Branch PIC X(3).
05 FILLER PIC X(2) VALUE SPACES.
05 DL-Desc PIC X(28).
05 FILLER PIC X(2) VALUE SPACES.
05 DL-Amount PIC Z,ZZZ,ZZ9.99.
05 FILLER PIC X VALUE SPACE.
05 DL-DC PIC X.
05 FILLER PIC X VALUE SPACE.
01 WS-Page-Total-Line.
05 FILLER PIC X(2) VALUE SPACES.
05 FILLER PIC X(38) VALUE SPACES.
05 FILLER PIC X(5) VALUE "PAGE ".
05 PT-Page PIC ZZ9.
05 FILLER PIC X(7) VALUE " TOTAL ".
05 FILLER PIC X(2) VALUE SPACES.
05 PT-Amount PIC Z,ZZZ,ZZ9.99.
05 FILLER PIC X(3) VALUE SPACES.
01 WS-Total-Line.
05 FILLER PIC X(2) VALUE SPACES.
05 TL-Label PIC X(53).
05 FILLER PIC X(2) VALUE SPACES.
05 TL-Amount PIC Z,ZZZ,ZZ9.99CR.
05 FILLER PIC X VALUE SPACE.
01 WS-Count-Line.
05 FILLER PIC X(2) VALUE SPACES.
05 CL-Label PIC X(53).
05 FILLER PIC X(10) VALUE SPACES.
05 CL-Count PIC ZZ,ZZ9.
05 FILLER PIC X VALUE SPACE.
PROCEDURE DIVISION.
Main-Paragraph.
PERFORM Start-Report
PERFORM Read-Next-Txn
PERFORM UNTIL End-Of-File
PERFORM Print-Detail
PERFORM Read-Next-Txn
END-PERFORM
PERFORM End-Page
PERFORM Print-Summary
PERFORM End-Report
STOP RUN.
Start-Report.
OPEN INPUT Txn-File
IF WS-Txn-Status NOT = "00"
DISPLAY "Cannot open the transaction file, status " WS-Txn-Status
STOP RUN
END-IF
OPEN OUTPUT Report-File
IF WS-Rpt-Status NOT = "00"
DISPLAY "Cannot open the report file, status " WS-Rpt-Status
STOP RUN
END-IF
*> The run stamp is set once and printed on every page header.
MOVE FUNCTION CURRENT-DATE TO WS-Now
MOVE WS-Now-Ymd TO HD-Date
MOVE WS-Now-Hh TO HD-Hour
MOVE WS-Now-Mi TO HD-Minute
MOVE WS-Now-Ss TO HD-Second
PERFORM Start-Page.
Read-Next-Txn.
READ Txn-File
AT END
SET End-Of-File TO TRUE
NOT AT END
ADD 1 TO WS-Read-Count
END-READ
IF WS-Txn-Status NOT = "00" AND WS-Txn-Status NOT = "10"
DISPLAY "Read failed, file status " WS-Txn-Status
STOP RUN
END-IF.
*> Page 1 starts where the paper is; every later page starts with a
*> form feed, which is what ADVANCING PAGE writes when the FD has no
*> LINAGE clause.
Start-Page.
ADD 1 TO WS-Page-No
MOVE WS-Page-No TO HD-Page
IF WS-Page-No = 1
WRITE Report-Line FROM WS-Head-1 AFTER ADVANCING 1 LINE
ELSE
WRITE Report-Line FROM WS-Head-1 AFTER ADVANCING PAGE
END-IF
WRITE Report-Line FROM WS-Head-2 AFTER ADVANCING 1 LINE
WRITE Report-Line FROM WS-Blank-Line AFTER ADVANCING 1 LINE
IF Summary-Page
WRITE Report-Line FROM WS-Head-Summary AFTER ADVANCING 1 LINE
ELSE
WRITE Report-Line FROM WS-Head-Cols AFTER ADVANCING 1 LINE
END-IF
WRITE Report-Line FROM WS-Rule-Line AFTER ADVANCING 1 LINE
MOVE WS-Header-Lines TO WS-Page-Line
MOVE 0 TO WS-Page-Total.
*> The page-break test. WS-Wanted holds the number of lines the caller
*> is about to write; the footer must still fit under them.
Check-Room.
COMPUTE WS-Needed = WS-Page-Line + WS-Wanted + WS-Footer-Lines
IF WS-Needed > WS-Page-Size
PERFORM End-Page
PERFORM Start-Page
END-IF.
Print-Detail.
MOVE 1 TO WS-Wanted
PERFORM Check-Room
MOVE Txn-Ref TO DL-Ref
MOVE Txn-Date TO DL-Date
MOVE Txn-Branch TO DL-Branch
MOVE Txn-Desc TO DL-Desc
MOVE Txn-Amount TO DL-Amount
MOVE Txn-DC TO DL-DC
WRITE Report-Line FROM WS-Detail-Line AFTER ADVANCING 1 LINE
ADD 1 TO WS-Page-Line
ADD 1 TO WS-Detail-Count
ADD Txn-Amount TO WS-Page-Total
EVALUATE Txn-DC
WHEN "D"
ADD Txn-Amount TO WS-Debit-Total
WHEN "C"
ADD Txn-Amount TO WS-Credit-Total
WHEN OTHER
DISPLAY "Unexpected type [" Txn-DC "] on " Txn-Ref
END-EVALUATE.
End-Page.
WRITE Report-Line FROM WS-Rule-Line AFTER ADVANCING 1 LINE
MOVE WS-Page-No TO PT-Page
MOVE WS-Page-Total TO PT-Amount
WRITE Report-Line FROM WS-Page-Total-Line AFTER ADVANCING 1 LINE
ADD WS-Footer-Lines TO WS-Page-Line.
*> The grand totals always start a fresh page. To let them share the
*> last detail page instead, replace the PERFORM Start-Page below with
*> MOVE WS-Summary-Lines TO WS-Wanted and PERFORM Check-Room.
Print-Summary.
SET Summary-Page TO TRUE
PERFORM Start-Page
WRITE Report-Line FROM WS-Blank-Line AFTER ADVANCING 1 LINE
WRITE Report-Line FROM WS-Rule-Line AFTER ADVANCING 1 LINE
MOVE "RECORDS READ" TO CL-Label
MOVE WS-Read-Count TO CL-Count
WRITE Report-Line FROM WS-Count-Line AFTER ADVANCING 1 LINE
MOVE "DEBIT TOTAL" TO TL-Label
MOVE WS-Debit-Total TO TL-Amount
WRITE Report-Line FROM WS-Total-Line AFTER ADVANCING 1 LINE
MOVE "CREDIT TOTAL" TO TL-Label
MOVE WS-Credit-Total TO TL-Amount
WRITE Report-Line FROM WS-Total-Line AFTER ADVANCING 1 LINE
COMPUTE WS-Net = WS-Debit-Total - WS-Credit-Total
MOVE "NET (DEBITS LESS CREDITS)" TO TL-Label
MOVE WS-Net TO TL-Amount
WRITE Report-Line FROM WS-Total-Line AFTER ADVANCING 1 LINE
MOVE "PAGES PRINTED" TO CL-Label
MOVE WS-Page-No TO CL-Count
WRITE Report-Line FROM WS-Count-Line AFTER ADVANCING 1 LINE
ADD WS-Summary-Lines TO WS-Page-Line.
End-Report.
CLOSE Txn-File Report-File
IF WS-Rpt-Status NOT = "00"
DISPLAY "Close of the report file failed, status " WS-Rpt-Status
END-IF
MOVE WS-Read-Count TO WS-Show-Count
DISPLAY "Records read : " WS-Show-Count
MOVE WS-Detail-Count TO WS-Show-Count
DISPLAY "Detail lines : " WS-Show-Count
MOVE WS-Page-No TO WS-Show-Count
DISPLAY "Pages printed : " WS-Show-Count.
The input record
The copybook, shipped as /code/cobol-write-report-file-page-breaks.cpy, describes the 64 bytes the reader consumes:
01 Txn-Record.
05 Txn-Date PIC 9(8).
05 Txn-Ref PIC X(6).
05 Txn-Branch PIC X(3).
05 Txn-Desc PIC X(28).
05 Txn-Amount PIC 9(9)V99.
05 Txn-DC PIC X.
05 FILLER PIC X(7).
| Field | Positions | PIC | Meaning |
|---|---|---|---|
| Txn-Date | 1 to 8 | 9(8) | YYYYMMDD |
| Txn-Ref | 9 to 14 | X(6) | Reference, T00001 upwards |
| Txn-Branch | 15 to 17 | X(3) | Branch code |
| Txn-Desc | 18 to 45 | X(28) | Description, space padded |
| Txn-Amount | 46 to 56 | 9(9)V99 | Eleven digits, implied decimal point, unsigned |
| Txn-DC | 57 | X | D for debit, C for credit |
| FILLER | 58 to 64 | X(7) | Reserved, spaces |
The first record of the shipped file is 20260901T00001CRKSUPPLIER INVOICE 00000430825D, followed by seven spaces and a line feed. The amount carries no sign, for the reason given in the sequential-file lesson: a separate type byte is unambiguous across languages and encodings in a way that an overpunched sign is not.
How the page break works
Everything about pagination lives in four constants and one paragraph. The page holds 30 usable lines; the header costs 5 and the footer costs 2, which leaves 23 for detail lines. Check-Room is the whole mechanism:
Check-Room.
COMPUTE WS-Needed = WS-Page-Line + WS-Wanted + WS-Footer-Lines
IF WS-Needed > WS-Page-Size
PERFORM End-Page
PERFORM Start-Page
END-IF.
WS-Page-Line is the hand-kept line counter: Start-Page sets it to the header size after writing the header, every detail line adds one to it, and End-Page adds the footer size. WS-Wanted is how many lines the caller is about to write, which is why the same paragraph serves a one-line detail and, if you want it to, a seven-line summary block. The footer is always included in the test, so a page never fills up so completely that its own total has nowhere to go. That is the mistake this arrangement exists to prevent.
The writing itself is one statement shape repeated. Per section 7.8.50 of the Programmer's Guide, the FROM phrase "will cause literal-1 or identifier-1 to be automatically moved into record-name-1 prior to writing record-name-1's contents", so each line is composed in WORKING-STORAGE, where it can have VALUE clauses and named fields, and copied into the file record on the way out. AFTER ADVANCING is the direction that matters: the guide says that with AFTER "the ADVANCING action writes line-terminator characters to the file and then the record data is written", while BEFORE writes the record first. Reports use AFTER because a printer positions the paper and then prints.
ADVANCING PAGE is the page eject. With no LINAGE clause in the FD, the guide is explicit about what GnuCOBOL emits: the clause "will introduce an ASCII formfeed character into the file", before the record for AFTER PAGE. Page 1 does not use it, because a form feed as the first byte of the file would eject a blank sheet and show up as a stray control character in a text editor; every later page starts with one.
Worked example: the boundary at record 24
Follow the counter through the first page. Start-Page writes five header lines and sets WS-Page-Line to 5. Detail records 1 to 23 each pass the test and each add one, so after record 23 the counter reads 28.
Record 24 arrives. Print-Detail moves 1 into WS-Wanted and performs Check-Room, which computes 28 + 1 + 2 = 31. That exceeds the page size of 30, so End-Page writes the rule and the page-total line, Start-Page writes a form feed and the page-2 header, the counter is reset to 5, and only then is record 24 printed as the first detail line of page 2. The same thing happens at record 47, which lands alone on page 3.
Those numbers come from the Python script in /code/cobol-write-report-file-page-breaks.py, which writes the data file and then builds the report using the same constants and the same edited pictures. Running it prints the arithmetic:
wrote 47 records of 64 bytes to datasets\cobol-write-report-file-page-breaks.dat
page size 30 lines = 5 header + 23 detail + 2 footer
records read : 47
detail lines/page: 23
pages printed : 4 (3 detail pages + 1 summary page)
report lines : 80
detail lines : 47
page 1 total : 93,787.36
page 2 total : 130,760.42
page 3 total : 4,653.04
debit total : 98,658.31
credit total : 130,542.51
net (D less C) : 31,884.20CR
page break : record 24 arrives with the line counter at 28 on page 1; 28 + 1 + 2 > 30
page break : record 47 arrives with the line counter at 28 on page 2; 28 + 1 + 2 > 30
| Page | Detail lines | Lines used | Page total |
|---|---|---|---|
| 1 | 23 | 30 | 93,787.36 |
| 2 | 23 | 30 | 130,760.42 |
| 3 | 1 | 8 | 4,653.04 |
| 4 (summary) | 0 | 12 | none |
The three page totals add to 229,200.82, and so do the debit and credit totals: 98,658.31 plus 130,542.51. A control total that cross-foots against a total taken a different way is the point of printing it at all. The net is negative, and it prints as 31,884.20CR, because PIC Z,ZZZ,ZZ9.99CR puts the credit symbol after the digits and leaves two spaces when the value is not negative.
What comes out
The last two pages of the report, with the page-3 boundary and the summary block:
DAILY TRANSACTION REGISTER PAGE 3
RUN 2026/09/07 07:52:31
REF DATE BR DESCRIPTION AMOUNT T
------------------------------------------------------------------------
T00047 2026/09/02 LMK DIRECT DEBIT 4,653.04 C
------------------------------------------------------------------------
PAGE 3 TOTAL 4,653.04
DAILY TRANSACTION REGISTER PAGE 4
RUN 2026/09/07 07:52:31
REPORT SUMMARY
------------------------------------------------------------------------
------------------------------------------------------------------------
RECORDS READ 47
DEBIT TOTAL 98,658.31
CREDIT TOTAL 130,542.51
NET (DEBITS LESS CREDITS) 31,884.20CR
PAGES PRINTED 4
The date and time in the header come from FUNCTION CURRENT-DATE, which section 8.1.12 of the guide documents as a 21-character result laid out as year, month, day, hours, minutes, seconds, hundredths, then a signed offset from Greenwich; the program takes the first eight characters as a PIC 9(8) and moves them into PIC 9(4)/9(2)/9(2), where the slashes are simple insertion characters. The function takes no arguments and, as the guide notes, no parentheses.
The LINAGE and END-OF-PAGE alternative
COBOL can keep the line counter for you. Put a LINAGE clause on the FD and the compiler generates a LINAGE-COUNTER special register for that file, and the WRITE statement can then tell you when a page is full:
FD Report-File
LINAGE IS 30 LINES
LINES AT TOP 3
LINES AT BOTTOM 3
WITH FOOTING AT 28.
01 Report-Line PIC X(72).
*> ...and in the PROCEDURE DIVISION:
WRITE Report-Line FROM WS-Detail-Line AFTER ADVANCING 1 LINE
AT END-OF-PAGE
PERFORM End-Page
PERFORM Start-Page
END-WRITE
The four numbers describe the paper. Section 6.2.1 of the guide defines LINES AT TOP and LINES AT BOTTOM as the unused margins, LINAGE IS n LINES as the "total number of used/usable lines on the page", and FOOTING AT as the "line number beyond which nothing may be printed except for any footing that is to appear on every page", adding that the three page numbers "should be the total number of possible lines available on one printed page". Here they add to 36, which is a short page; a 66-line page at six lines per inch would be LINAGE IS 60 with three lines top and bottom.
FOOTING AT 28 reproduces the break in the listing above exactly. IBM's Language Reference states the rule crisply: an end-of-page condition occurs when a WRITE "causes the value in the LINAGE-COUNTER special register to equal or exceed the value specified in the WITH FOOTING phrase", and "The WRITE statement is executed, and then the END-OF-PAGE imperative-statement is executed". Five header lines plus 23 detail lines put the counter at 28, so the twenty-third detail line prints and the AT END-OF-PAGE branch runs after it, which is where the footer and the next header belong.
LINAGE and AT END-OF-PAGE are language features, documented as such by both compilers; the counter in the main listing is ordinary program logic, which is why it runs anywhere and survives being ported. The reasons to keep the manual version are that the arithmetic is visible, the same test handles a multi-line block, and nothing about the output depends on how a particular runtime interprets a logical page. The reason to prefer LINAGE is that the counter cannot drift out of step with what was actually written, since the register "cannot be modified" by the program and is maintained by the runtime. GnuCOBOL also ships a third option: section 2.1.13 of the guide describes its implementation of the Report Writer Control System, "fully implemented as of version 3.0", which takes over pagination, headings and totals through declarations in a REPORT SECTION.
What the two dialects do
| Question | GnuCOBOL 3.x Programmer's Guide | IBM Enterprise COBOL 6.4 Language Reference |
|---|---|---|
| ADVANCING omitted | AFTER ADVANCING 1 LINE on a line-advancing file, BEFORE ADVANCING 1 LINE otherwise | "as if AFTER ADVANCING 1 LINE had been specified" |
| ADVANCING PAGE, no LINAGE | writes an ASCII form feed into the file | repositions to line 1 of the next page |
| ADVANCING PAGE, with LINAGE | writes line terminators to reach the next page, "No formfeed characters will be generated" | repositions to the first printable line of the next page, per the clause |
| AT END-OF-PAGE | fires when the write lands in the footing area defined by LINAGE | requires LINAGE in the FD; fires when LINAGE-COUNTER reaches FOOTING |
| LINAGE-COUNTER | BINARY-LONG SIGNED, one per file with LINAGE, "cannot be modified" | initialised to 1 by OPEN, reset to 1 by ADVANCING PAGE, must not be modified |
| Where LINAGE is allowed | the FD of a sequential or line sequential file; a sequential file becomes line sequential | "effective for sequential files opened as OUTPUT or EXTEND" |
| ADVANCING on other organizations | rejected for relative and indexed files, and can corrupt a record sequential file | not supported for VSAM files |
Two of those rows are worth pausing on. The default differs between the dialects, so a WRITE with no ADVANCING phrase is not portable and is best written out in full. And on z/OS the line-sequential format of WRITE offers AFTER ADVANCING only, with no BEFORE and no END-OF-PAGE phrase; a print report there is normally a sequential file whose FD carries the LINAGE clause.
Compile and run
python code/cobol-write-report-file-page-breaks.py
cobc -x -I code -o write-register code/cobol-write-report.cob
./write-register
Run all three from the folder that holds code/ and datasets/, since both ASSIGN clauses name paths relative to the working directory. -I code tells cobc where the copybook lives. The guide notes in section 5.2.1 that if the compiler configuration has filename mapping enabled, the runtime first looks for an environment variable to supply the real path, which is how the same program reads a different file under a different job.
Limitations
The output is a text file, and text files lose trailing spaces. The GnuCOBOL Manual documents COB_LS_FIXED as defaulting to false, meaning line sequential files are variable length "by removing trailing spaces", so a 72-character record ends at its last non-blank character. That is invisible in a printed report and fatal to a downstream program that expects fixed-length records; set COB_LS_FIXED if something is going to read the report back. The same appendix documents COB_LS_VALIDATE as on by default, rejecting non-display data on a write with status 71, which is a reason to keep binary fields out of print lines.
Form feeds are the other soft spot. Because AFTER performs the advance before the data, the first WRITE emits its line terminator ahead of the title, so the file opens with an empty line, and at a page boundary the advance is a form feed rather than a newline; the guide's wording implies the previous page's last line and the next page's title are separated by that form feed alone. A pager or a printer handles this correctly, a plain text editor may not, and we did not run the program to confirm the byte order. Whether the final line of the file receives a terminator at CLOSE is not stated in the guide either, so check it on your build if something downstream cares.
Two smaller ones. The detail amount picture holds seven integer digits while the input field holds nine, so an amount of ten million or more would be silently truncated on the high order; size print fields from the largest value the data can hold, not the typical one. And the Programmer's Guide used throughout here says on its own first page that it documents the July 2020 build of GnuCOBOL 3.1 RC-1, so anything that changed in 3.2 would not appear in it. Where the two GnuCOBOL documents disagree, they disagree about line endings: section 5.2.1.2 lists MinGW among the builds that write a bare line feed, while the WRITE section puts MinGW with native Windows on carriage return plus line feed. Do not build a record layout on top of that difference without testing it.
Correction, 29 October 2026: the build command shown before named the source cobol-write-report-file-page-breaks.cob, and GnuCOBOL 3.2's cobc refuses a source file whose base name is longer than 31 characters ("invalid file base name ... length exceeds maximum"), even with -o, so it failed as printed. The listing is now shipped as cobol-write-report.cob, and the old download address redirects there.
Index terms cobol, gnucobol, write, report, page break