The previous post covered IBM i PTF management and OS upgrades — understanding PTF types, using WRKPTF and DSPPTF, managing SF99xxx group PTFs, and planning a Technology Refresh. This post covers display files and 5250 UX modernisation: the DDS-based screen programming model that has driven IBM i operator interfaces for decades, how RPG interacts with those screens, subfile programming, indicator management, and the realistic options for replacing or augmenting green-screen interfaces with modern web front-ends in 2026.
What Display Files Are
A display file (DSPF) is a compiled IBM i object that describes one or more screen record formats. Each record format defines fields, their positions, display attributes, function-key indicators, and error messages. The file is created from DDS (Data Description Specifications) source using the CRTDSPF command. The resulting *FILE object of type DSPF lives in a library and is opened by an RPG, COBOL, or CL program just like a database file.
When a program opens a display file and writes a record format to it, the 5250 data stream is sent to the workstation session. The operator sees the rendered screen, fills in fields, and presses a function key or Enter. The program then reads the record format back, receiving the field data and the active indicator that identifies which key was pressed.
Display files are compiled objects — changes to the DDS source require recompiling the DSPF and, in many cases, recompiling the RPG program that references it (if level-check is active). This is one reason display file changes have traditionally required a change-control window.
Basic DDS Display File Syntax
DDS source for a display file lives in a source physical file — typically QDDSSRC in your development library. Each line follows a fixed-column layout:
- Columns 6–16: record/field name area
- Column 17: type indicator (R = record format, blank = field)
- Column 38 onward: keywords
A minimal single-record display file looks like this:
A R CUSTINQ CF03(03 'Exit')
A CF12(12 'Cancel')
A BLINK
A OVERLAY
A MSGLOC(24)
A CUSTNO 6Y 0B 8 10EDTCDE(Z)
A DSPATR(UL)
A CUSTNM 40A O 8 20
A ERRMSG1 79A O 24 2
A DSPATR(HI)
Key DDS keywords explained:
- CF03(03 ‘Exit’) — assigns indicator 03 when F3 is pressed
- CF12(12 ‘Cancel’) — assigns indicator 12 when F12 is pressed
- BLINK — cursor blinks on this record
- OVERLAY — writes this record without erasing other records already on screen
- MSGLOC(24) — message line is row 24
- B (column 38, usage) — input/output field (both input and output)
- O — output-only field
- DSPATR(UL) — underline display attribute
- DSPATR(HI) — high-intensity (bright) display attribute
- EDTCDE(Z) — edit code Z (suppress leading zeros)
Conditional keywords use indicator conditioning. For example, to show an error message only when indicator 50 is on:
A R CUSTINQ
A 50 ERRMSG('Customer not found' 50)
A CUSTNO 6Y 0B 8 10
The 50 in column 9 means the keyword applies only when IN50 is set to '1' before the write.
Creating a Display File with CRTDSPF
Compile a display file from DDS source with:
CRTDSPF FILE(DEVLIB/CUSTINQDF)
SRCFILE(DEVLIB/QDDSSRC)
SRCMBR(CUSTINQDF)
AUT(*USE)
TEXT('Customer Inquiry Display File')
Here is a more complete DDS example — a customer inquiry screen with a header record and a detail record:
A* ============================================================
A* CUSTINQDF - Customer Inquiry Display File
A* ============================================================
A DSPSIZ(24 80 *DS3)
A PRINT
A INDARA
A*
A* -- Header record (constant text, title, function key legend) --
A R HEADER
A OVERLAY
A 1 2'AS400 DECODED'
A 1 30'CUSTOMER INQUIRY'
A DSPATR(HI)
A 1 65DATE
A EDTCDE(Y)
A 1 75TIME
A 23 2'F3=Exit'
A 23 12'F5=Refresh'
A 23 25'F12=Cancel'
A*
A* -- Detail record (input/output fields) ----------------------
A R DETAIL CF03(03 'Exit')
A CF05(05 'Refresh')
A CF12(12 'Cancel')
A OVERLAY
A MSGLOC(24)
A 50 ERRMSG('Customer number not found' 50)
A 51 ERRMSG('Customer number is required' 51)
A PROMCUST 30A O 5 2VALUE('Enter Customer Number:')
A CUSTNO 6Y 0B 5 35EDTCDE(Z)
A DSPATR(UL)
A CHECK(RZ)
A 30 DSPATR(PR)
A CUSTNM 40A O 8 5
A CUSTADDR 40A O 9 5
A CUSTCITY 30A O 10 5
A CUSTSTATE 2A O 10 38
A CUSTZIP 10A O 10 43
A CUSTPHONE 15A O 12 5
A CUSTSTATUS 10A O 13 5
A DSPATR(HI)
The INDARA keyword at the file level tells the system to use a separate 99-byte indicator area rather than embedding indicators within the data buffer — this is the modern approach and required for the free-format RPG indicator techniques shown below.
The DSPSIZ(24 80 *DS3) keyword specifies a 24-row by 80-column display in 3270-compatible mode, the standard 5250 terminal size. CHECK(RZ) right-adjusts and zero-fills a numeric input field.
RPG Free-Format Display File I/O
In free-format RPG IV (ILE RPG), you declare a display file in the file definition using DCL-F:
**FREE
Ctl-Opt DftActGrp(*No) ActGrp('CUSTINQ') Option(*SrcStmt *NoDebugIO);
// ── File declarations ────────────────────────────────────────────────
DCL-F CUSTINQDF WORKSTN INDDS(Indic) INFDS(WsInfo) USROPN;
// ── Indicator data structure (maps to the 99-byte INDARA) ────────────
DCL-DS Indic LEN(99);
In03 IND POS(3); // F3 = Exit
In05 IND POS(5); // F5 = Refresh
In12 IND POS(12); // F12 = Cancel
In30 IND POS(30); // Protect CUSTNO field
In50 IND POS(50); // Customer not found error
In51 IND POS(51); // Customer number required error
END-DS;
// ── File information data structure ─────────────────────────────────
DCL-DS WsInfo INFDS(CUSTINQDF);
WsFunction CHAR(1) POS(369); // AID byte (function key pressed)
END-DS;
// ── Standalone fields ────────────────────────────────────────────────
DCL-S WkCustNo PACKED(6:0);
DCL-S WkCustNm CHAR(40);
DCL-S WkAddr CHAR(40);
DCL-S WkCity CHAR(30);
DCL-S WkState CHAR(2);
DCL-S WkZip CHAR(10);
DCL-S WkPhone CHAR(15);
DCL-S WkStatus CHAR(10);
// ────────────────────────────────────────────────────────────────────
// Main procedure
// ────────────────────────────────────────────────────────────────────
DCL-PROC Main;
OPEN CUSTINQDF;
WRITE HEADER;
DOW '1';
// Clear error indicators before each display
In50 = *OFF;
In51 = *OFF;
In30 = *OFF;
EXFMT DETAIL; // Write screen to workstation, wait for keypress
SELECT;
WHEN In03 = *ON; // F3 pressed — exit
LEAVE;
WHEN In12 = *ON; // F12 pressed — cancel / return to menu
LEAVE;
WHEN In05 = *ON; // F5 pressed — refresh without processing
ITER;
OTHER; // Enter pressed — process the request
EXSR ReadCustomer;
ENDSL;
ENDDO;
CLOSE CUSTINQDF;
*INLR = *ON;
END-PROC;
// ────────────────────────────────────────────────────────────────────
// Subroutine: read customer from DB2 for i
// ────────────────────────────────────────────────────────────────────
DCL-PROC ReadCustomer;
IF WkCustNo = 0;
In51 = *ON; // Required field
RETURN;
ENDIF;
// Chain to CUSTMST physical file
CHAIN WkCustNo CUSTMST;
IF %FOUND(CUSTMST);
WkCustNm = CMNAME;
WkAddr = CMADDR;
WkCity = CMCITY;
WkState = CMSTATE;
WkZip = CMZIP;
WkPhone = CMPHONE;
WkStatus = CMSTATUS;
In30 = *ON; // Protect the CUSTNO field — show result, don't allow edit
ELSE;
In50 = *ON; // Customer not found
WkCustNm = *BLANKS;
WkAddr = *BLANKS;
ENDIF;
END-PROC;
Key points in this code:
- INDDS(Indic) — binds the file’s indicator area to the
Indicdata structure, so you reference named Boolean fields (In03,In50) instead of*IN03,*IN50. - EXFMT — a combined WRITE + READ. It sends the record format to the screen and waits for the operator to respond. This is the standard display file interaction verb.
- CHAIN — random read by key from a database file;
%FOUNDtests whether the record was located. - Indicators are set to
*ONor*OFFbefore each EXFMT, controlling conditional DDS keywords such as ERRMSG and DSPATR(PR).
Subfile Programming
A subfile is a scrollable list of records displayed within a display file. It requires two record formats: the SFL (subfile) record that defines one row of the list, and the SFLCTL (subfile control) record that manages the overall subfile — page size, display, clear, and end-of-file markers.
DDS for a basic order-list subfile:
A* ORDLISTDF - Order List Subfile Display File
A DSPSIZ(24 80 *DS3)
A INDARA
A*
A* -- Subfile record (one row per order) -----------------------
A R ORDROW SFL
A SFLRRN 4Y 0H
A ORDNUM 7Y 0O 8 3EDTCDE(Z)
A ORDDATE L O 8 13EDTCDE(Y)
A CUSTNUM 6Y 0O 8 22EDTCDE(Z)
A ORDAMT 11Y 2O 8 32EDTCDE(1)
A ORDSTAT 10A O 8 47
A*
A* -- Subfile control record ------------------------------------
A R ORDCTL SFLCTL(ORDROW)
A CF03(03 'Exit')
A CF07(07 'Page Up')
A CF08(08 'Page Down')
A OVERLAY
A SFLSIZ(0200)
A SFLPAG(0014)
A 41 SFLDSP
A 42 SFLDSPCTL
A 43 SFLCLR
A 44 SFLEND(*MORE)
A 6 3'Order#'
A 6 13'Date'
A 6 22'Cust#'
A 6 32'Amount'
A 6 47'Status'
A DSPATR(HI)
In this DDS, four indicators on SFLCTL control the subfile lifecycle:
- IN41 on → SFLDSP: display the subfile records
- IN42 on → SFLDSPCTL: display the control record (the header row with column labels)
- IN43 on → SFLCLR: clear the subfile before loading new records
- IN44 on → SFLEND(*MORE): show “+ More” at the bottom when more records exist
The RPG subfile loading loop in free format:
**FREE
// ── Subfile load procedure ─────────────────────────────────────────
DCL-PROC LoadOrderSubfile;
DCL-S SubRrn PACKED(4:0) INZ(0);
DCL-S MaxRows PACKED(4:0) INZ(0);
// Step 1: Clear the existing subfile
In43 = *ON; // SFLCLR on
In41 = *OFF; // SFLDSP off (must be off during clear)
In42 = *OFF; // SFLDSPCTL off
In44 = *OFF; // SFLEND off
WRITE ORDCTL;
In43 = *OFF; // Turn clear off after write
// Step 2: Load records into the subfile
SubRrn = 0;
MaxRows = 0;
SETLL *LOVAL ORDHDR;
DOW '1';
READ ORDHDR;
IF %EOF(ORDHDR);
LEAVE;
ENDIF;
SubRrn += 1;
SFLRRN = SubRrn;
ORDNUM = OHORDNO;
ORDDATE = OHORDT;
CUSTNUM = OHCUST;
ORDAMT = OHAMT;
ORDSTAT = OHSTAT;
WRITE ORDROW;
MaxRows += 1;
IF MaxRows >= 1000; // Safety limit — prevent runaway loads
LEAVE;
ENDIF;
ENDDO;
// Step 3: Display the subfile
IF MaxRows > 0;
In41 = *ON; // SFLDSP on — show records
In42 = *ON; // SFLDSPCTL on — show control / header
In44 = *OFF; // SFLEND: turn on if truncated at limit
IF MaxRows >= 1000;
In44 = *ON;
ENDIF;
ELSE;
In41 = *OFF; // No records — just show the control header
In42 = *ON;
ENDIF;
EXFMT ORDCTL; // Display and wait for operator response
END-PROC;
Paging in subfiles is handled automatically by the 5250 data stream when SFLPAG and SFLSIZ are set. SFLPAG(0014) means 14 rows are visible at once; SFLSIZ(0200) means the subfile buffer holds up to 200 records. The terminal handles Page Up / Page Down scrolling within the already-loaded buffer without RPG involvement.
For very large result sets, a paginate-on-demand approach loads one page at a time. The program tracks the current page cursor, reloads the subfile on each Page Down keypress, and adjusts SFLEND accordingly.
Indicator Variables in Free RPG
IBM i display files rely heavily on indicators — 99 numbered single-bit flags that control screen behaviour. In traditional fixed-format RPG, these were referenced as *IN03, *IN50 etc., scattered throughout the program with little documentation of meaning. This is the classic “indicator spaghetti”.
Free-format RPG with INDARA and a named indicator data structure eliminates this problem:
**FREE // Named indicator data structure — maps to the 99-byte INDARA buffer DCL-DS Ind LEN(99); // Navigation KF3_Exit IND POS(3); KF5_Refresh IND POS(5); KF12_Cancel IND POS(12); // Screen control ProtectInput IND POS(30); // Error conditions ErrNotFound IND POS(50); ErrRequired IND POS(51); ErrDuplicate IND POS(52); ErrDateRange IND POS(53); // Subfile control SflDisplay IND POS(41); SflDisplayCtl IND POS(42); SflClear IND POS(43); SflMoreRecords IND POS(44); END-DS;
With named indicators, every reference in the RPG code is self-documenting. Setting ErrNotFound = *ON is immediately clear; setting *IN50 = *ON is not. The rule is simple: always use INDARA with a named DS in any new or refactored display file program.
Another technique is to define a separate indicator DS per record format, keeping each record’s indicator set cleanly isolated:
// Per-record indicator DSes DCL-DS DetailInd LEN(99); DI_F3Exit IND POS(3); DI_F12Cancel IND POS(12); DI_Protect IND POS(30); DI_ErrMsg1 IND POS(50); END-DS; DCL-DS SflInd LEN(99); SI_F3Exit IND POS(3); SI_F7PageUp IND POS(7); SI_F8PageDown IND POS(8); SI_SflDsp IND POS(41); SI_SflDspCtl IND POS(42); SI_SflClr IND POS(43); SI_SflEnd IND POS(44); END-DS;
Both DSes share the same underlying 99-byte INDARA buffer (because the display file was opened with a single INDDS reference). Positions overlap intentionally — indicators 3 (F3) and 12 (F12) appear in both because both records respond to those keys.
Modernisation Path Options
IBM i 5250 green-screen applications continue to run reliably in 2026. However, there are legitimate business drivers to modernise: training new staff on 5250 terminals is harder, customers increasingly expect web or mobile interfaces, and organisations want consistent UX standards across all their applications. The choices are not binary.
Option 1 — Profound UI and Screen Transformation Tools
Profound UI (from Profound Logic) intercepts the 5250 data stream and dynamically renders the screen content as a web page in a browser. No RPG source changes are required for a basic transformation. The tool parses record format metadata from the compiled DSPF object and maps fields to HTML/CSS elements.
A basic Profound UI configuration entry (in its Node.js-based server configuration):
// profound_config.js (excerpt)
module.exports = {
host: 'ibmi.example.com',
port: 23, // Standard Telnet / 5250 port
ssl: true,
sslPort: 992,
theme: 'carbon', // IBM Carbon design system
appName: 'AS400 Decoded Customer Inquiry',
screenMap: {
'CUSTINQDF/DETAIL': {
layout: 'responsive',
fieldOverrides: {
CUSTNO: { type: 'number', label: 'Customer Number', placeholder: 'Enter 6-digit ID' },
CUSTNM: { type: 'text', label: 'Customer Name', readOnly: true },
}
}
}
};
Lansa offers a similar transformation product. Both tools sit in front of the existing 5250 application, meaning your existing RPG investment is preserved. The limitation is that the underlying UX remains constrained by what 5250 can represent — you cannot add drag-and-drop or real-time charts without also changing the RPG source.
Option 2 — Node.js REST API Approach
This approach, discussed in earlier posts in this series, builds a new web front-end that calls IBM i business logic via REST APIs rather than via display files. The display file RPG program is refactored to separate the business logic into a service program (*SRVPGM), which is then exposed via a Node.js or PHP REST layer:
// ibmi-customer-api.js — Node.js REST layer calling DB2 for i via ODBC
const express = require('express');
const odbc = require('odbc');
const app = express();
app.use(express.json());
const DB_CONN = 'DSN=IBMI_PROD;UID=APIUSER;PWD=secret';
app.get('/api/customers/:id', async (req, res) => {
let conn;
try {
conn = await odbc.connect(DB_CONN);
const custNo = parseInt(req.params.id, 10);
if (isNaN(custNo) || custNo console.log('Customer API listening on port 3000'));
The web front-end — React, Vue, or plain HTML — calls this API. The 5250 display file is retired. The RPG business logic (validation, calculations, workflow) is preserved in the service program and called by the API layer. This is the most complete modernisation but requires the most investment.
Option 3 — IBM i Access Client Solutions
IBM i Access Client Solutions (ACS) is IBM’s free downloadable Java application that provides 5250 emulation with a modern GUI shell, database browsing, IFS navigator, and SQL scripting. It is the recommended replacement for the older System i Navigator and iSeries Access for Windows clients.
For organisations that need to retain 5250 access but want a supportable, modern-looking client, ACS is the pragmatic choice. Its 5250 emulator supports macros, keyboard remapping, and screen customisation via GUI configuration — without touching IBM i source code. Deploying ACS across the organisation standardises the client environment and removes dependency on ageing PC hardware that ran the old ActiveX-based clients.
# ACS deployment via silent install (Windows, MSI wrapper) msiexec /i "IBMiACS.msi" /quiet /norestart ^ INSTALLDIR="C:Program FilesIBMClientSolutions" ^ ADDLOCAL=ALL ^ /l*v "C:Logsacs_install.log"
When Green Screen Is Still Valid
Not every IBM i interface needs a web UI. There are categories of screen that are legitimate long-term green-screen citizens in 2026:
- Internal operational tools — batch job submission screens, WRKJOB, WRKSPLF, STRSEU: these are used by IBMi-skilled staff who are faster on 5250 than on any web equivalent.
- DBA and system administration screens — STRSQL, WRKOBJ, WRKLIBPDM: these are purpose-built for IBM i expertise and require no modernisation.
- High-frequency data-entry screens — order entry used by trained operators who have memorised keystrokes. 5250 is exceptionally fast for keyboard-centric, eyes-on-data entry: no mouse, no tab-to-next-field confusion, direct function-key navigation.
- Screens that are genuinely stable — if a screen has not changed in 10 years and the users are content, the ROI of modernisation is negative.
The modernisation decision should be driven by user pain, business requirements, or integration needs — not by the assumption that all green screens are bad. A realistic IBM i shop in 2026 typically has a mixture: new customer-facing workflows on the web, internal operational screens still on 5250, and a small number of high-value operator screens transformed with Profound UI.
Next post: IBM i and Apache Kafka — connecting IBM i to event streaming platforms, journal-based change data capture to Kafka topics, and consuming Kafka messages from IBM i using Node.js and Python in PASE.