IBM i Automation
Automate Excel and CSV Reporting Directly from IBM i
Build a controlled IBM i reporting workflow with SYSTOOLS.GENERATE_SPREADSHEET, including reusable SQL definitions, protected IFS output, scheduling, delivery, retention, privacy, diagnostics, and lifecycle tracking.
Many IBM i reporting processes still depend on a person running a query, exporting the result, renaming the file, moving it to a shared directory, and emailing it to someone. SYSTOOLS.GENERATE_SPREADSHEET makes it possible to move that work into a repeatable SQL-driven process—but the production design must manage more than file creation.
The service can generate:
csv
txt
ods
xls
xlsx
from either:
- a complete database file
- an inline SQL query
- an SQL query stored in an IFS file
The output is written directly to the integrated file system.
The most important question is not:
Can IBM i create an Excel file?
It can.
The better question is:
How do we make the report repeatable, secure, supportable, and safe to deliver?
The basic function
A simple database-file export is:
VALUES SYSTOOLS.GENERATE_SPREADSHEET(
PATH_NAME => '/reports/customers.csv',
LIBRARY_NAME => 'MYLIB',
FILE_NAME => 'CUSTOMER',
SPREADSHEET_TYPE => 'csv',
COLUMN_HEADINGS => 'COLUMN'
);
The function returns:
1 Success
-1 Failure
A query-driven workbook can be generated with:
VALUES SYSTOOLS.GENERATE_SPREADSHEET(
PATH_NAME =>
'/reports/open_orders.xlsx',
SPREADSHEET_QUERY =>
'SELECT ORDER_NUMBER,
CUSTOMER_NUMBER,
ORDER_DATE,
ORDER_TOTAL
FROM MYLIB.ORDERS
WHERE ORDER_STATUS = ''OPEN''
ORDER BY ORDER_DATE',
SPREADSHEET_TYPE =>
'xlsx',
COLUMN_HEADINGS =>
'COLUMN',
SHEET_NAME =>
'Open Orders'
);
Supported output types
IBM documents the following lowercase values:
csv
ods
txt
xls
xlsx
The default is:
csv
Choose the format based on the receiving process.
Use CSV when:
- another system will ingest the data
- simple tabular output is sufficient
- broad interoperability matters
- workbook formatting is unnecessary
Use XLSX when:
- people will open the report in Excel-compatible software
- multiple columns require readable widths
- an existing workbook template is used
- a sheet name or starting position matters
Use ODS when:
- OpenDocument compatibility is required
- spreadsheet placement features are needed without Excel format
Use TXT when:
- a delimited text result is appropriate
- the recipient expects text rather than a workbook
The file extension should match the requested type.
Column headings
The heading options are:
NONE
COLUMN
LABEL
NONE produces no heading row and is the default.
COLUMN uses SQL column names.
LABEL uses database column labels and falls back to the column name when no label exists.
Example:
VALUES SYSTOOLS.GENERATE_SPREADSHEET(
PATH_NAME =>
'/reports/customer_labels.xlsx',
LIBRARY_NAME =>
'MYLIB',
FILE_NAME =>
'CUSTOMER',
SPREADSHEET_TYPE =>
'xlsx',
COLUMN_HEADINGS =>
'LABEL'
);
For business-facing reports, explicit aliases in a query are often clearer than depending on physical column names.
SELECT
ORDER_NUMBER AS "Order Number",
CUSTOMER_NAME AS "Customer",
ORDER_DATE AS "Order Date",
ORDER_TOTAL AS "Order Total"
FROM MYLIB.ORDERS;
Store reusable queries in the IFS
An inline query is limited to 4,000 characters.
Longer or source-controlled report definitions can be stored in the IFS:
/reports/sql/monthly_sales.sql
Then run:
VALUES SYSTOOLS.GENERATE_SPREADSHEET(
PATH_NAME =>
'/reports/output/monthly_sales.xlsx',
SPREADSHEET_QUERY_IFS =>
'/reports/sql/monthly_sales.sql',
SPREADSHEET_TYPE =>
'xlsx',
COLUMN_HEADINGS =>
'COLUMN',
SHEET_NAME =>
'Monthly Sales'
);
IBM requires the IFS query file to:
Use CCSID 1208 / UTF-8
Contain a single supported query
Reference fully qualified database objects
Avoid QTEMP objects
Not end with a semicolon
An IFS query file is useful because it can be:
- version controlled
- reviewed independently
- promoted between environments
- reused by multiple jobs
- compared during an audit
- changed without embedding a long string in RPG or CL
Use a report-definition table
Do not hard-code every output path and query inside a scheduler entry.
Create a controlled report catalogue:
CREATE TABLE REPORTING.REPORT_DEFINITION
(
REPORT_ID VARCHAR(40) PRIMARY KEY,
REPORT_NAME VARCHAR(256) NOT NULL,
QUERY_IFS_PATH VARCHAR(1024) NOT NULL,
OUTPUT_DIRECTORY VARCHAR(1024) NOT NULL,
OUTPUT_TYPE VARCHAR(10) NOT NULL,
COLUMN_HEADINGS VARCHAR(10) NOT NULL,
SHEET_NAME VARCHAR(128),
OVERWRITE_POLICY VARCHAR(10) NOT NULL,
RETENTION_DAYS INTEGER NOT NULL,
CONTAINS_SENSITIVE_DATA CHAR(1) NOT NULL,
DELIVERY_METHOD VARCHAR(20) NOT NULL,
IS_ENABLED CHAR(1) NOT NULL,
UPDATED_BY VARCHAR(128) NOT NULL,
UPDATED_TIMESTAMP TIMESTAMP NOT NULL
);
The catalogue defines approved behavior.
A report request should reference:
REPORT_ID
REPORT_DATE
ENVIRONMENT
REQUESTED_BY
rather than accepting an unrestricted SQL query or arbitrary IFS path.
Keep report SQL separate from report execution
A clean design separates:
What data the report contains
How the report is generated
Where the file is written
Who receives it
How long it is retained
The SQL query belongs in a reviewed source file.
The report catalogue owns output and policy.
The scheduler or processor owns timing and lifecycle.
The delivery component owns transfer.
This separation makes changes easier to review and reduces accidental data exposure.
Build predictable file names
Avoid repeatedly overwriting a generic file when history matters.
A practical pattern is:
report-id_YYYYMMDD_HHMMSS.xlsx
Example:
open-orders_20260727_220500.xlsx
You can build the path in SQL:
VALUES
'/reports/output/open-orders_'
CONCAT VARCHAR_FORMAT(
CURRENT TIMESTAMP,
'YYYYMMDD_HH24MISS'
)
CONCAT '.xlsx';
For rerunnable reports, include a unique request identifier:
open-orders_20260727_220500_req10422.xlsx
This avoids two jobs silently writing to the same path.
Use protected IFS directories
Do not write business reports into a broadly accessible location merely because it is convenient.
Avoid defaulting to:
/tmp
for files containing business, personal, financial, health, or security information.
A controlled layout can be:
/reports/
sql/
work/
outbound/
archive/
failed/
Example authorities:
/reports/sql Report administrators and deployment process
/reports/work Report service profile only
/reports/outbound Delivery service and approved support profiles
/reports/archive Retention process and auditors
/reports/failed Restricted support access
The execution profile needs authority to traverse each parent directory and create or update the file.
Generate into a work directory first
Do not create the report directly in a directory watched by an SFTP process or downstream consumer.
Use:
/reports/work
for generation.
After successful validation, move the completed file into:
/reports/outbound
This prevents another process from reading a partially written workbook.
A safe lifecycle is:
REQUESTED
RUNNING
GENERATED
VALIDATED
READY_FOR_DELIVERY
DELIVERED
ARCHIVED
DELETED
FAILED
Track every report run
CREATE TABLE REPORTING.REPORT_RUN
(
RUN_ID BIGINT
GENERATED ALWAYS AS IDENTITY,
REPORT_ID VARCHAR(40) NOT NULL,
REQUESTED_TIMESTAMP TIMESTAMP NOT NULL,
STARTED_TIMESTAMP TIMESTAMP,
COMPLETED_TIMESTAMP TIMESTAMP,
REQUESTED_BY VARCHAR(128) NOT NULL,
EXECUTION_JOB VARCHAR(28),
OUTPUT_PATH VARCHAR(1024),
OUTPUT_TYPE VARCHAR(10),
STATUS VARCHAR(30) NOT NULL,
FUNCTION_RESULT INTEGER,
ROW_COUNT BIGINT,
FILE_SIZE_BYTES BIGINT,
DELIVERY_STATUS VARCHAR(30),
DELIVERED_TIMESTAMP TIMESTAMP,
RETENTION_DATE DATE,
ERROR_SQLCODE INTEGER,
ERROR_SQLSTATE CHAR(5),
ERROR_MESSAGE VARCHAR(2048)
);
This table answers:
- Was the report requested?
- Did generation start?
- Which job ran it?
- Where is the file?
- Did the function return success?
- How many rows were expected?
- Was the file delivered?
- When should it be deleted?
- Why did it fail?
Count the rows before generating
For an approved report definition, capture the expected row count before file creation.
Example:
SELECT COUNT(*)
FROM MYLIB.ORDERS
WHERE ORDER_STATUS = 'OPEN';
Store it in REPORT_RUN.
After generation, use the value as a reasonableness check.
A successful function result does not necessarily prove the report contains the intended business population.
Examples of problems that still require detection:
Zero rows when thousands were expected
A filter date resolved incorrectly
The wrong environment was queried
A join multiplied the row count
A source table was not refreshed
A report ran before the upstream process completed
Replace versus update
The OVERWRITE option supports:
REPLACE
UPDATE
REPLACE is the default.
It replaces the contents of the existing file.
UPDATE writes into an existing XLS or XLSX file using the configured starting sheet, row, and column without replacing existing formatting.
Example:
VALUES SYSTOOLS.GENERATE_SPREADSHEET(
PATH_NAME =>
'/reports/templates/monthly_template.xlsx',
SPREADSHEET_QUERY_IFS =>
'/reports/sql/monthly_sales.sql',
SPREADSHEET_TYPE =>
'xlsx',
OVERWRITE =>
'UPDATE',
STARTING_SHEET =>
2,
STARTING_ROW =>
5,
STARTING_COLUMN =>
'B'
);
UPDATE is supported only for:
xls
xlsx
Positioning options are supported for:
xls
xlsx
ods
Use workbook templates carefully
A reusable template can already contain:
- titles
- formulas
- charts
- branding
- instructions
- hidden calculation sheets
- conditional formatting
The process should copy a protected master template to a unique work-file path before calling UPDATE.
Do not update the master copy directly.
Recommended flow:
1. Copy the approved template.
2. Generate into the copied workbook.
3. Validate the output.
4. Publish the completed file.
5. Preserve the unchanged master template.
Assign a sheet name
Current support includes:
SHEET_NAME
for:
xls
xlsx
ods
Example:
VALUES SYSTOOLS.GENERATE_SPREADSHEET(
PATH_NAME =>
'/reports/output/customer_export.xlsx',
SPREADSHEET_QUERY =>
'SELECT CUSTOMER_NUMBER,
CUSTOMER_NAME,
CUSTOMER_STATUS
FROM MYLIB.CUSTOMER
ORDER BY CUSTOMER_NUMBER',
SPREADSHEET_TYPE =>
'xlsx',
COLUMN_HEADINGS =>
'COLUMN',
SHEET_NAME =>
'Customers'
);
The sheet-name enhancement is delivered at:
IBM i 7.6 — Db2 Group PTF SF99960 Level 3
IBM i 7.5 — Db2 Group PTF SF99950 Level 12
Earlier positioning, IFS-query, update, and daemon options were delivered at the immediately preceding Db2 Group PTF levels.
LOB values require special handling
CLDownload does not directly support:
CLOB
BLOB
DBCLOB
XML
A small character LOB can be cast:
SELECT
MESSAGE_ID,
CAST(MESSAGE_TEXT AS VARCHAR(32000))
AS MESSAGE_TEXT
FROM MYLIB.MESSAGE_STORE;
This introduces a maximum length.
The cast can truncate the value if it is too small.
For large text, XML, or binary data:
- export the object separately
- include a stable identifier in the spreadsheet
- include a protected IFS link or reference
- avoid placing large documents inside a business report
Character encoding and delimiters
A CSV file that looks correct in one desktop application may be interpreted differently by another system.
Confirm:
- character encoding
- decimal separator
- date format
- timestamp format
- quoting
- embedded commas
- embedded line breaks
- null representation
- leading zeros
- spreadsheet formula interpretation
For machine integration, define a data contract.
Do not assume that a file called CSV is self-describing.
Use explicit SQL formatting when necessary:
SELECT
VARCHAR_FORMAT(
ORDER_DATE,
'YYYY-MM-DD'
) AS ORDER_DATE,
DECIMAL(
ORDER_TOTAL,
15,
2
) AS ORDER_TOTAL
FROM MYLIB.ORDERS;
Prevent spreadsheet-formula injection
Text beginning with characters such as:
=
+
-
@
can be interpreted as a formula by spreadsheet software.
This matters when exported values contain user-supplied text.
Examples include:
- customer names
- comments
- descriptions
- external identifiers
- imported notes
Assess whether the receiving workbook software can execute formulas or external references.
For high-risk data, sanitize or prefix values according to the organization’s reporting standard.
Do not modify legitimate numeric values blindly.
The rule should be applied only to columns intended to be plain text.
Protect personal and regulated information
A database table may be properly secured while its exported workbook is not.
Before generating a report, review:
- row permissions
- column masking
- RCAC behavior
- adopted authority
- execution profile
- query ownership
- IFS directory authority
- delivery destination
- email or SFTP encryption
- retention
- deletion
- backup copies
- downstream sharing
Common sensitive content includes:
Personal identifiers
Payment information
Health information
Employee data
Customer contact details
Security configuration
Maintenance gaps
Authentication data
The generated file must be governed as a new copy of the data.
Do not email unrestricted attachments by default
Email is convenient but can create uncontrolled copies in:
- sender mailboxes
- recipient mailboxes
- mobile devices
- mail archives
- security gateways
- forwarded messages
- backup systems
Alternatives include:
- secure SFTP delivery
- a controlled portal
- time-limited download
- a protected file share
- encrypted delivery
- notification containing a secure link
The report catalogue should define the approved method for each report.
Separate generation from delivery
GENERATE_SPREADSHEET creates the file.
It does not provide a complete delivery, retry, confirmation, or retention workflow.
Use separate components:
Report generator
File validator
Delivery worker
Delivery confirmation
Archive process
Retention cleanup
Monitoring and alerting
A delivery failure should not force the report query to be rerun automatically if the already-generated file is valid.
Retry the failed stage.
Validate the generated file
Minimum validation can include:
- file exists
- file size is greater than zero
- expected extension
- expected row count was not zero unexpectedly
- output path is under an approved directory
- generation result is
1 - file was created after the run started
Additional validation can include:
- checksum
- duplicate detection
- workbook-open test
- expected headings
- maximum file size
- virus or malware scanning in the receiving environment
- delivery encryption confirmation
Understand the ACS dependency
GENERATE_SPREADSHEET uses the CLDownload feature in:
/QIBM/proddata/Access/ACS/Base/acsbundle.jar
IBM delivers this JAR through the IBM HTTP Server for i Group PTF.
This means the report process depends on more than the Db2 Group PTF.
Review:
- installed HTTP Group PTF
- installed ACS JAR level
- Java availability
- IFS authority
- Qshell execution support
- current IBM fixes
As of July 2026, IBM identifies ACS base package version:
1.1.9.13
with a May 2026 build.
The host JAR and desktop ACS packages should be maintained through the organization’s approved IBM i maintenance process.
QJVAEXEC daemon jobs
ACS Java support can leave a service daemon visible as:
QJVAEXEC
This can be normal and allows later requests by the same user to reuse the service.
Use:
KILL_DAEMON => 'YES'
when the daemon threads should be ended after the request:
VALUES SYSTOOLS.GENERATE_SPREADSHEET(
PATH_NAME =>
'/reports/work/daily.csv',
SPREADSHEET_QUERY_IFS =>
'/reports/sql/daily.sql',
SPREADSHEET_TYPE =>
'csv',
KILL_DAEMON =>
'YES'
);
The default is:
NO
Keeping the daemon can help repeated execution.
Ending it can be useful for isolated batch runs or controlled job cleanup.
Failure diagnostics
When no existing STDOUT override is present, CLDownload output is redirected to:
QTEMP/QGENSPREAD
When the function returns -1, inspect that file in the same job.
Because QTEMP belongs to the job, a support person in another job cannot directly see it.
A production processor should copy or summarize diagnostics before the generating job ends.
If STDOUT is already overridden, the output is not redirected to QTEMP/QGENSPREAD.
This should be included in support documentation.
Capture SQL diagnostics
A SQL procedure can capture the failure state:
BEGIN
DECLARE V_RESULT INTEGER DEFAULT 0;
DECLARE V_SQLCODE INTEGER DEFAULT 0;
DECLARE V_SQLSTATE CHAR(5) DEFAULT '00000';
DECLARE V_MESSAGE VARCHAR(2048) DEFAULT '';
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1
V_MESSAGE = MESSAGE_TEXT;
SET V_SQLCODE = SQLCODE;
SET V_SQLSTATE = SQLSTATE;
END;
SET V_RESULT =
SYSTOOLS.GENERATE_SPREADSHEET(
PATH_NAME =>
'/reports/work/open_orders.xlsx',
SPREADSHEET_QUERY_IFS =>
'/reports/sql/open_orders.sql',
SPREADSHEET_TYPE =>
'xlsx',
COLUMN_HEADINGS =>
'COLUMN',
SHEET_NAME =>
'Open Orders',
KILL_DAEMON =>
'YES'
);
INSERT INTO REPORTING.REPORT_RUN
(
REPORT_ID,
REQUESTED_TIMESTAMP,
COMPLETED_TIMESTAMP,
REQUESTED_BY,
OUTPUT_PATH,
OUTPUT_TYPE,
STATUS,
FUNCTION_RESULT,
ERROR_SQLCODE,
ERROR_SQLSTATE,
ERROR_MESSAGE
)
VALUES
(
'OPEN_ORDERS',
CURRENT TIMESTAMP,
CURRENT TIMESTAMP,
SESSION_USER,
'/reports/work/open_orders.xlsx',
'xlsx',
CASE
WHEN V_RESULT = 1
AND V_SQLSTATE = '00000'
THEN 'GENERATED'
ELSE 'FAILED'
END,
V_RESULT,
V_SQLCODE,
V_SQLSTATE,
V_MESSAGE
);
END;
Adapt handler behavior to the local SQL procedure standard.
MFA considerations
IBM documents two restrictions:
GENERATE_SPREADSHEETcannot run when the profile’s TOTP interval has expired.- A profile used by a batch or scheduled job cannot use
*TOTPas its authentication method.
Do not weaken MFA for interactive users to make scheduled reporting work.
Use an approved non-interactive service profile designed for automation.
The profile should have:
- no unnecessary interactive sign-on
- minimum database authority
- minimum IFS authority
- controlled job description
- documented ownership
- appropriate credential policy
- auditable use
Avoid QTEMP dependencies
Queries supplied through SPREADSHEET_QUERY or SPREADSHEET_QUERY_IFS cannot reference QTEMP objects.
This affects report designs that first populate a temporary table.
Alternatives include:
- a common table expression
- a view
- a declared global temporary table replaced with a persistent work table
- a report-specific staging table keyed by run ID
- a user-defined table function
- a stored procedure that materializes approved report data
Clean up persistent staging data after the report lifecycle completes.
Schedule only after upstream readiness
Running at 2:00 a.m. does not guarantee that the source data is ready at 2:00 a.m.
Use a dependency condition:
Source batch completed
Control total accepted
Business date closed
Upstream interface reconciled
Required table refresh completed
The scheduler should evaluate a readiness table or lifecycle record before generating the report.
This avoids distributing incomplete data successfully.
Prevent duplicate scheduled runs
Use a unique key:
REPORT_ID + BUSINESS_DATE + SCHEDULE_INSTANCE
Example:
CREATE UNIQUE INDEX
REPORTING.REPORT_RUN_U1
ON REPORTING.REPORT_RUN
(
REPORT_ID,
REPORT_DATE,
SCHEDULE_INSTANCE
);
If REPORT_DATE and SCHEDULE_INSTANCE are needed, add them to the table definition.
A rerun should require:
- a new approved attempt number
- a new output path
- a reason
- linkage to the original run
Retention and deletion
Each report definition should have a retention policy.
Example:
Work files Delete after delivery confirmation
Outbound files Retain 2 days
Archived reports Retain 30 days
Failed files Retain 7 days for support
Audit metadata Retain according to policy
Deletion should be recorded.
A report marked DELETED should contain:
Deletion timestamp
Deletion job
Deletion result
Approved retention rule
Do not delete a file still required by an investigation, legal hold, audit, or reconciliation process.
Monitoring and alerting
Useful alerts include:
- report generation failed
- expected row count is zero
- file exceeds an approved size
- file was generated but not delivered
- delivery failed repeatedly
- output remains in the work directory
- retention cleanup failed
- duplicate run was rejected
- query definition changed without approval
- ACS or Java dependency failed
Avoid repeated alerts for the same unchanged failure.
Link every alert to:
RUN_ID
REPORT_ID
OUTPUT_PATH
EXECUTION_JOB
ERROR_MESSAGE
OWNER
A practical architecture
Scheduler or request
|
v
Report-definition lookup
|
v
Upstream readiness check
|
v
Create REPORT_RUN row
|
v
Build unique protected work path
|
v
GENERATE_SPREADSHEET
|
v
Validate file and row expectations
|
v
Move to outbound directory
|
v
Deliver through approved channel
|
v
Confirm delivery
|
v
Archive and retain
|
v
Delete according to policy
When GENERATE_SPREADSHEET is a strong fit
Use it when:
- the source is a Db2 for i table or SQL query
- the result is tabular
- CSV, text, ODS, XLS, or XLSX is appropriate
- the report can be generated in batch
- SQL defines the business population
- output belongs in the IFS
- delivery and retention can be controlled separately
When another approach is better
Use a document-generation tool when:
- pixel-perfect pagination is required
- the output contains complex narrative text
- charts and layout must be dynamically designed
- PDF is the required primary format
Use an API when:
- the consumer requires real-time data
- incremental retrieval is needed
- a file exchange creates unnecessary latency
- a stable machine contract is more appropriate
Use an ETL or data-platform tool when:
- very large data volumes are involved
- multiple source systems must be combined
- transformation and lineage requirements exceed a report query
Detailed SQL reference
For a parameter-by-parameter implementation guide, see:
Generate Excel, CSV, ODS, and Text Files from IBM i SQL
Final takeaway
SYSTOOLS.GENERATE_SPREADSHEET solves the file-generation step.
A production reporting process must also solve:
Definition
Approval
Scheduling
Data readiness
Security
Output naming
Validation
Delivery
Retry
Retention
Deletion
Monitoring
Auditability
The modernized process is not:
Run SQL and email a spreadsheet.
It is:
Generate the right report, from the right data, under the right authority, deliver it through an approved channel, prove what happened, and remove it when it is no longer needed.
Comments
Share your thoughts, questions, or real-world IBM i experiences related to this article.