COBOL group items and elementary items: the record layout as a struct
How level numbers 01 to 49, 77 and 88 build a record; what a group move does to the bytes; a worked 80-byte customer record with its byte offsets.
A COBOL record is a struct whose fields sit at fixed byte offsets, declared by indenting level numbers instead of braces. This tutorial builds one 80-byte customer record, prints it as raw bytes and as fields, and then shows the one behaviour that trips up people arriving from other languages: a MOVE to the group copies bytes, while a MOVE to an elementary item converts and pads. The complete program is below. It was first desk-checked against the GnuCOBOL 3.2 Programmer's Guide; on 29 October 2026 we compiled and ran it with GnuCOBOL 3.2, and the output shown is what it printed. It builds on the PICTURE clause and the MOVE truncation rules from the fundamentals lessons.
The program
>>SOURCE FORMAT IS FREE
*> cobol-group-items.cob — a customer record as a group item
*> Dialect: GnuCOBOL 3.x, free format
*> Status : desk-checked against the GnuCOBOL 3.2 Programmer's Guide;
*> compiled and run with GnuCOBOL 3.2 on 2026-10-29
*> Inputs : none
*> Outputs: DISPLAY lines on the console
*> Build : cobc -x cobol-group-items.cob
*> Run : ./cobol-group-items
*> (Windows: cobol-group-items.exe)
IDENTIFICATION DIVISION.
PROGRAM-ID. GROUP-ITEMS.
ENVIRONMENT DIVISION.
DATA DIVISION.
WORKING-STORAGE SECTION.
*> One 80-byte record. The 01 level is the group; every item with a
*> PICTURE is elementary; Cust-Address and Last-Payment-Date are
*> groups inside the group.
01 Customer-Record.
05 Cust-Id PIC 9(6) VALUE 104233.
05 Cust-Name PIC X(20) VALUE "MARGARET OKONKWO".
05 Cust-Address.
10 Addr-Street PIC X(18) VALUE "17 HARBOUR ROW".
10 Addr-City PIC X(10) VALUE "GALWAY".
10 Addr-Postcode PIC X(7) VALUE "H91X2Y3".
05 Cust-Balance PIC S9(7)V99 SIGN IS LEADING SEPARATE
VALUE -1250.75.
05 Cust-Status PIC X VALUE "A".
88 Cust-Active VALUE "A".
88 Cust-Closed VALUE "C".
05 Last-Payment-Date.
10 LPD-Year PIC 9(4) VALUE 2026.
10 LPD-Month PIC 9(2) VALUE 8.
10 LPD-Day PIC 9(2) VALUE 29.
*> Level 77 items stand alone; they can never have subordinates.
77 WS-Record-Length PIC 9(3).
77 WS-Balance-Out PIC -(7)9.99.
PROCEDURE DIVISION.
Main-Paragraph.
MOVE FUNCTION LENGTH(Customer-Record) TO WS-Record-Length
DISPLAY "Record length : " WS-Record-Length
DISPLAY "Raw record : [" Customer-Record "]"
DISPLAY "Name : [" Cust-Name "]"
DISPLAY "City : [" Addr-City "]"
DISPLAY "Balance raw : " Cust-Balance
MOVE Cust-Balance TO WS-Balance-Out
DISPLAY "Balance edited: " WS-Balance-Out
DISPLAY "Paid on : " LPD-Year "-" LPD-Month "-" LPD-Day
IF Cust-Active
DISPLAY "Status : active"
END-IF
*> Elementary moves: each receiving field pads, aligns or converts
*> according to its own PICTURE.
MOVE "DUBLIN" TO Addr-City
MOVE 9 TO LPD-Month
MOVE 1 TO LPD-Day
ADD 100 TO Cust-Balance
SET Cust-Closed TO TRUE
DISPLAY "After moves : [" Customer-Record "]"
*> A group move copies bytes left to right and converts nothing,
*> so text lands in the numeric fields underneath.
MOVE "2026-09-05" TO Last-Payment-Date
DISPLAY "Group move : [" Last-Payment-Date "] year=" LPD-Year
MOVE SPACES TO Cust-Address
DISPLAY "Cleared addr : [" Cust-Address "]"
STOP RUN.
Level numbers, group items, elementary items
The Programmer's Guide (section 2.1.7, Structured Data) puts it plainly: a data item that has subordinates is a group item, and an item with no subordinates is an elementary item. Only elementary items carry a PICTURE (section 6.9.33 says the clause "may only be specified on an elementary item"), and a group item's size is simply the sum of everything underneath it. That is the whole mechanism. Level numbers from 01 to 49 express nesting: an item is subordinate to the nearest preceding item with a smaller level number. The actual numbers are arbitrary as long as they are consistent, which is why shops write 01, 05, 10, 15 and leave gaps for later insertions.
Two special levels appear in the listing. Level 77 declares a standalone elementary item that can never have subordinates and never belongs to a group; we use it for the two scratch fields at the end of WORKING-STORAGE. Level 88 declares a condition name: it is not storage at all but a named test on the item immediately above it. Cust-Active is true when Cust-Status holds "A", and SET Cust-Closed TO TRUE is the idiomatic way to store "C" without the caller knowing the code. The VALUE clause (section 6.9.51) has a different meaning at level 88, where it lists the values that make the condition true, and at other levels, where it is the compile-time initial value of the storage.
The word FILLER, which does not appear in this program but appears in almost every real copybook, names an item you never intend to reference by name: padding, reserved space, or a field the program does not care about. GnuCOBOL and IBM both let you omit the name entirely on such items.
Byte offsets
Because every elementary item has a fixed size, every field has a fixed offset. Working them out by hand is how you read a hexadecimal dump of a mainframe file, so here is the table for Customer-Record.
| Item | PICTURE | Bytes | Positions |
|---|---|---|---|
| Cust-Id | 9(6) | 6 | 1 to 6 |
| Cust-Name | X(20) | 20 | 7 to 26 |
| Addr-Street | X(18) | 18 | 27 to 44 |
| Addr-City | X(10) | 10 | 45 to 54 |
| Addr-Postcode | X(7) | 7 | 55 to 61 |
| Cust-Balance | S9(7)V99 SIGN LEADING SEPARATE | 10 | 62 to 71 |
| Cust-Status | X | 1 | 72 |
| LPD-Year | 9(4) | 4 | 73 to 76 |
| LPD-Month | 9(2) | 2 | 77 to 78 |
| LPD-Day | 9(2) | 2 | 79 to 80 |
Cust-Address is the 35 bytes from position 27 to 61 and Last-Payment-Date is the 8 bytes from 73 to 80; the whole record is 80 bytes, which FUNCTION LENGTH (section 8.1.36) confirms at run time. Two details matter in that table. The V in S9(7)V99 is an assumed decimal point and occupies no storage, so the balance has nine digit positions. The S would normally be folded into one of the digit bytes as an overpunched sign, which makes the raw bytes hard to read in a demonstration; SIGN IS LEADING SEPARATE asks for a real "-" or "+" character in front of the digits instead, at the cost of one extra byte. The USAGE table in section 6.9.49 lists exactly this accounting: one byte per 9, X, A or editing symbol, plus one for a separate sign.
Line by line
The first DISPLAY prints the length through a three-digit work field rather than displaying the function result directly, so the output format is predictable: 080. The second prints the group item in square brackets. A group is treated as alphanumeric whenever it is used as a whole, so DISPLAY shows the raw 80 bytes: the id, the name padded with spaces to 20, the three address fields each padded to their own width, the balance as the ten characters "-000125075", the status letter, and the date digits run together. The next lines print the same information field by field. Displayed on its own, Cust-Balance comes out as -0001250.75, not as its ten stored characters: GnuCOBOL's default pretty-display setting inserts the decimal point at the V when a numeric item is displayed by itself, while inside the group the same bytes print exactly as stored. WS-Balance-Out, an edited picture, turns the balance into something a person would print on a statement.
The block of elementary moves is where the conversion happens. MOVE "DUBLIN" TO Addr-City left-justifies the six letters and pads the remaining four positions with spaces. MOVE 9 TO LPD-Month stores "09", because a numeric receiving field is aligned on its decimal point and zero-filled. ADD 100 TO Cust-Balance does signed arithmetic on the nine digits and rewrites the separate sign. Each receiving field applies its own PICTURE to whatever arrives.
The group move is the contrast. MOVE "2026-09-05" TO Last-Payment-Date targets an 8-byte group. IBM's language reference states the rule for group moves: the move "is treated as though it were an alphanumeric-to-alphanumeric elementary move, except that there is no conversion of data from one form of internal representation to another." So the ten-character literal is copied left to right, truncated to eight bytes, and the digits and hyphens land wherever they fall: LPD-Year gets "2026", which happens to be fine, but LPD-Month now holds "-0" and LPD-Day holds "9-". Nothing warns you. The last statement, MOVE SPACES TO Cust-Address, is the benign and very common form of the same thing: filling a whole group with spaces in one statement.
Compile and run
cobc -x cobol-group-items.cob
./cobol-group-items
The first line of the source is a >>SOURCE FORMAT IS FREE directive placed in column 8, so the file compiles in cobc's default fixed mode and in -free mode alike; the directive and the -free option are equivalent according to section 2.1.16 of the Programmer's Guide.
Worked example
Console output of the compiled program (GnuCOBOL 3.2, 29 October 2026):
Record length : 080
Raw record : [104233MARGARET OKONKWO 17 HARBOUR ROW GALWAY H91X2Y3-000125075A20260829]
Name : [MARGARET OKONKWO ]
City : [GALWAY ]
Balance raw : -0001250.75
Balance edited: -1250.75
Paid on : 2026-08-29
Status : active
After moves : [104233MARGARET OKONKWO 17 HARBOUR ROW DUBLIN H91X2Y3-000115075C20260901]
Group move : [2026-09-] year=2026
Cleared addr : [ ]
Read the raw record against the offset table: positions 62 to 71 are the balance with its leading sign, position 72 is the status, and the last eight bytes are the date. After the elementary moves, the city, the balance, the status and the day have all changed in place, and the record is still exactly 80 bytes because nothing in COBOL can change a field's size at run time. The edited balance line is 11 characters wide because -(7)9.99 reserves seven floating positions, one fixed digit, the point and two decimals.
Why this matters for files
Every sequential file lesson on this site declares its records this way, and the reason is that a group item and a file record are the same thing: a byte array with named slices. When a program reads a 60-byte customer file, the READ places 60 bytes into a group under the FD and the program picks the fields out by name, with no parsing step. The sequential file tutorial does exactly that with a copybook, and the copybook is nothing more than the level-number tree from this lesson saved in its own file.
Limitations
Two things this program does not show. First, the sign format: with the default embedded sign, the last digit of a negative S9(7)V99 field is stored as an overpunched character that differs between ASCII and EBCDIC systems, so a raw DISPLAY would not look like the tidy "-000125075" above; we chose SIGN IS LEADING SEPARATE to keep the bytes readable, and you should expect the overpunched form in real mainframe data. Second, GnuCOBOL follows the standard in requiring 01 and 77 to begin in Area A only in fixed format; in free format there are no column rules, but the level-number nesting rules are unchanged. Also note that FUNCTION LENGTH returns characters, not bytes; for single-byte data the two are the same, and the Programmer's Guide points to BYTE-LENGTH when they are not. IBM Enterprise COBOL supports the same intrinsic but its documentation on maximum PICTURE length and on national data differs in detail, so check the IBM page linked above before relying on this for DBCS or UTF-8 fields.
Correction, 29 October 2026: the "Balance raw" line read -000125075. A real GnuCOBOL 3.2 run prints -0001250.75, because pretty-display inserts the decimal point when the elementary item is displayed on its own (the raw record line, which displays the group, does show -000125075); the line and the explanation above are corrected. The build command shown before also named the source cobol-group-items-elementary-items.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-group-items.cob, and the old download address redirects there. With the line corrected, the output shown matches a real run line for line, so the page now says the program was compiled and run.
Index terms cobol, gnucobol, group item, level number, record layout