Most IBM i developers spend their entire careers working with physical files and never touch the IFS. That is understandable — the traditional database model handles most of what a business application needs. But the moment you have to read a CSV dropped by a trading partner, write a JSON response for an API, generate a PDF report, or process an XML feed from an external system, you need the IFS. And if you have never used it before, it feels like a completely different operating system sitting inside the one you already know.
It is not that complicated. This post explains what the IFS is, how it is organised, and the practical patterns for reading and writing stream files from RPG.
What the IFS actually is
IFS stands for Integrated File System. It is IBM i’s hierarchical file system — the equivalent of the filesystem you use on Windows or Linux. It uses forward slashes, has directories and subdirectories, and stores files as streams of bytes rather than structured database records.
The traditional IBM i storage model (libraries, physical files, members) is still there. The IFS exists alongside it. From the IFS perspective, the traditional library structure is mounted at /QSYS.LIB. You can actually navigate to /QSYS.LIB/MYLIB.LIB/CUSTMAST.FILE and see your physical file from the IFS side.
The IFS root starts at /. Some important directories:
/home— home directories for users, like Linux/tmp— temporary files/QSYS.LIB— the traditional library structure, mounted here/QOpenSys— UNIX-compatible filesystem, case-sensitive/QDLS— Document Library Services, the old folder system (largely obsolete)
Most of the time you will be working under /home or a custom directory you create for your application — something like /myapp/input or /transfers/outbound.
Accessing the IFS from a green screen
Two commands you need to know:
WRKLNK (Work with Object Links) — the green-screen file browser for the IFS. Run it from any command line:
WRKLNK OBJ('/home/myuser')This gives you a list of files and directories, similar to WRKOBJ but for the IFS. From here you can display, copy, delete, and rename files.
MKDIR — create a directory:
MKDIR DIR('/myapp/input')
MKDIR DIR('/myapp/output')CPYTOSTMF / CPYFRMSTMF — copy between database files and stream files:
/* Copy a physical file member to an IFS stream file */
CPYTOSTMF FROMMBR('/QSYS.LIB/MYLIB.LIB/REPORT.FILE/DATA.MBR') +
TOSTMF('/home/myuser/report.txt') +
STMFOPT(*REPLACE) CVTDTA(*AUTO)
/* Copy an IFS stream file into a physical file member */
CPYFRMSTMF FROMSTMF('/home/myuser/inbound.csv') +
TOMBR('/QSYS.LIB/MYLIB.LIB/INBOUND.FILE/DATA.MBR') +
MBROPT(*REPLACE)Reading a stream file from RPG
RPG accesses IFS files through the integrated file system APIs. The key ones are open(), read(), write(), and close() — the same POSIX file I/O functions used in C and other languages.
You prototype them in RPG like this:
**FREE
ctl-opt dftactgrp(*no) actgrp(*caller);
// POSIX file API prototypes
dcl-pr ifsOpen int(10) extproc('open');
path pointer value options(*string);
openflags int(10) value;
mode uns(10) value options(*nopass);
end-pr;
dcl-pr ifsRead int(10) extproc('read');
filedesc int(10) value;
buffer pointer value;
nbytes uns(10) value;
end-pr;
dcl-pr ifsClose int(10) extproc('close');
filedesc int(10) value;
end-pr;
// Open flags
dcl-c O_RDONLY 1;
dcl-c O_WRONLY 2;
dcl-c O_CREAT 8;
dcl-c O_TRUNC 64;
dcl-s FileDesc int(10);
dcl-s Buffer char(4096);
dcl-s BytesRead int(10);
dcl-s FilePath varchar(256);
FilePath = '/myapp/input/orders.csv';
// Open for reading
FileDesc = ifsOpen(%addr(FilePath) + 2 : O_RDONLY);
if FileDesc 0;
// Process the buffer content
// For line-by-line processing, scan for newline characters
BytesRead = ifsRead(FileDesc : %addr(Buffer) : %size(Buffer));
enddo;
ifsClose(FileDesc);
*inlr = *on;
return;For most practical work, reading in chunks and scanning for newlines is how you process text files line by line.
A cleaner approach: Scott Klement’s IFSIO service program
The raw POSIX API works, but it is verbose to set up. Scott Klement’s free IFSIO service program (widely used across the IBM i community) wraps these APIs into easier prototypes and adds useful helpers for line-by-line reading, character set conversion, and error handling.
If your shop already has it installed, use it. If not, the raw POSIX approach above works on any IBM i system with no dependencies.
Writing a stream file from RPG
Writing follows the same pattern — open with write flags, write your data, close:
**FREE
ctl-opt dftactgrp(*no) actgrp(*caller);
dcl-pr ifsOpen int(10) extproc('open');
path pointer value options(*string);
openflags int(10) value;
mode uns(10) value options(*nopass);
end-pr;
dcl-pr ifsWrite int(10) extproc('write');
filedesc int(10) value;
buffer pointer value;
nbytes uns(10) value;
end-pr;
dcl-pr ifsClose int(10) extproc('close');
filedesc int(10) value;
end-pr;
dcl-c O_WRONLY 2;
dcl-c O_CREAT 8;
dcl-c O_TRUNC 64;
dcl-c S_IRWXU 448;
dcl-s FileDesc int(10);
dcl-s FilePath varchar(256);
dcl-s LineOut varchar(1024);
dcl-s CrLf char(2) inz(x'0d25'); // EBCDIC newline
FilePath = '/myapp/output/report.txt';
// Open for writing, create if not exists, truncate if exists
FileDesc = ifsOpen(%addr(FilePath) + 2 : O_WRONLY + O_CREAT + O_TRUNC : S_IRWXU);
if FileDesc < 0;
dsply 'Could not create file';
*inlr = *on;
return;
endif;
// Write a line
LineOut = 'EmployeeID,FirstName,LastName,Salary' + CrLf;
ifsWrite(FileDesc : %addr(LineOut) + 2 : %len(LineOut));
// Write more lines in a loop here...
ifsClose(FileDesc);
*inlr = *on;
return;Character set conversion — the thing that trips everyone up
IBM i stores data in EBCDIC internally. IFS stream files are typically ASCII or UTF-8. When you write EBCDIC data to an IFS file without converting it, the file is unreadable on any other system.
The IFS APIs handle conversion through code pages. When you open a file, you can specify the coded character set identifier (CCSID):
dcl-pr ifsOpen_CCSID int(10) extproc('Qp0lOpen');
path pointer value options(*string);
openflags int(10) value;
mode uns(10) value;
ccsid uns(10) value;
end-pr;
// Open with UTF-8 (CCSID 1208) — auto-converts on read/write
FileDesc = ifsOpen_CCSID(%addr(FilePath) + 2 : O_WRONLY + O_CREAT + O_TRUNC : S_IRWXU : 1208);Common CCSIDs:
- 1208 — UTF-8 (use this for files going to web services, modern systems)
- 819 — ISO 8859-1 / Latin-1 (common for European partners)
- 437 — ASCII (US)
- 65535 — no conversion (raw bytes, use for binary files)
When in doubt, use UTF-8. It handles the full Unicode range and is what every modern system expects.
Practical patterns you will actually use
Check if a file exists before processing
dcl-pr ifsStat int(10) extproc('stat');
path pointer value options(*string);
statbuf pointer value;
end-pr;
dcl-ds StatBuf len(144);
dummy char(144);
end-ds;
dcl-s FilePath varchar(256);
dcl-s rc int(10);
FilePath = '/myapp/input/orders.csv';
rc = ifsStat(%addr(FilePath) + 2 : %addr(StatBuf));
if rc = 0;
// File exists, proceed
else;
// File does not exist
endif;Delete a file after processing
dcl-pr ifsUnlink int(10) extproc('unlink');
path pointer value options(*string);
end-pr;
ifsUnlink(%addr(FilePath) + 2);Rename / move a file
dcl-pr ifsRename int(10) extproc('rename');
oldpath pointer value options(*string);
newpath pointer value options(*string);
end-pr;
dcl-s OldPath varchar(256);
dcl-s NewPath varchar(256);
OldPath = '/myapp/input/orders.csv';
NewPath = '/myapp/archive/orders_20260517.csv';
ifsRename(%addr(OldPath) + 2 : %addr(NewPath) + 2);This is the standard pattern for archiving processed files — rename them to an archive directory with a timestamp in the name.
Using SQL to work with IFS files
DB2 for i includes table functions for IFS operations that let you avoid the POSIX API entirely for some use cases.
Read a file’s contents directly in SQL:
SELECT CAST(LINE AS VARCHAR(1000))
FROM TABLE(QSYS2.IFS_READ('/myapp/input/orders.csv')) AS FThis returns each line of the file as a row. You can then use SQL to filter, transform, and insert the data directly into a table — no RPG required for simple ETL jobs.
List files in a directory:
SELECT PATH_NAME, DATA_SIZE, LAST_USED_TIMESTAMP
FROM TABLE(QSYS2.IFS_OBJECT_STATISTICS('/myapp/input', '*NO')) AS F
WHERE OBJECT_TYPE = '*STMF'
ORDER BY LAST_USED_TIMESTAMP DESCWhen to use the IFS
The IFS is the right choice when:
- You are exchanging files with external systems (CSV, XML, JSON, EDI)
- You need to generate files for download (reports, exports)
- You are building or consuming REST APIs (reading/writing JSON)
- You need to store binary data — images, PDFs, attachments
- You are running open-source tools (Node.js, Python, PHP) that expect a standard filesystem
For data that stays within your IBM i applications, physical files and DB2 tables remain the right model. The IFS is the bridge between that world and everything outside it.
Next post: Modern integrations — connecting IBM i to REST APIs, JSON, and the outside world.