The previous post covered IBM i commitment control and transaction management — starting and ending commitment definitions with STRCMTCTL and ENDCMTCTL, controlling DB2 transaction scope with COMMIT and ROLLBACK in CL and RPG, journal-based commitment control with STRJRNPF, savepoints with SAVEPOINT and ROLLBACK TO SAVEPOINT in embedded SQL, handling commitment control errors, and designing reliable multi-file update patterns on IBM i. This post covers DB2 for i triggers: creating BEFORE and AFTER triggers with CREATE TRIGGER, choosing between FOR EACH ROW and FOR EACH STATEMENT granularity, accessing OLD and NEW transition variables for changed data, building audit logging triggers, enforcing cascading business rules across tables, creating INSTEAD OF triggers on views, and applying trigger best practices on IBM i in 2026.
What Are DB2 for i Triggers?
A DB2 for i trigger is a named SQL object that executes automatically in response to an INSERT, UPDATE, or DELETE operation on a specific table. Triggers enforce business rules, maintain audit trails, and cascade changes to related tables — all transparently to the application performing the DML. IBM i supports two trigger types:
- SQL triggers — defined entirely in SQL with CREATE TRIGGER; portable, stored in the database catalogue, and the recommended approach for new development on IBM i 7.2+
- External triggers — call an ILE program (RPG, CL, C) when the trigger fires; used for legacy integration or when the triggered action requires non-SQL operations
This post covers SQL triggers. External triggers are created with ADDPFTRG and are the older mechanism.
CREATE TRIGGER Syntax
-- Trigger anatomy
CREATE OR REPLACE TRIGGER schema.trigger_name
{BEFORE | AFTER | INSTEAD OF} -- when the trigger fires
{INSERT | UPDATE [OF col,...] | DELETE} -- which DML event
ON schema.table_name
REFERENCING -- transition variable aliases
{OLD [ROW] [AS] old_alias}
{NEW [ROW] [AS] new_alias}
{OLD TABLE AS old_table_alias} -- transition table (AFTER only)
{NEW TABLE AS new_table_alias}
FOR EACH {ROW | STATEMENT} -- granularity
MODE DB2ROW -- required on IBM i
[WHEN (condition)] -- optional filter
BEGIN
-- trigger body: SQL statements
END
AFTER INSERT Trigger: Audit Logging
The most common trigger use case on IBM i is audit logging — recording every change to a sensitive table (orders, payments, user profiles) in a separate audit table. An AFTER trigger fires after the DML statement succeeds, so the audit record is only written when the change commits.
-- Audit table for order changes
CREATE TABLE APPLIB.ORD_AUDIT_LOG (
AUDIT_TS TIMESTAMP NOT NULL DEFAULT CURRENT TIMESTAMP,
AUDIT_ACTION CHAR(1) NOT NULL, -- 'I' insert, 'U' update, 'D' delete
AUDIT_USER VARCHAR(18) NOT NULL DEFAULT CURRENT USER,
ORD_NO CHAR(10) NOT NULL,
CUST_NO CHAR(8),
STATUS_OLD CHAR(1),
STATUS_NEW CHAR(1),
AMOUNT_OLD DECIMAL(11,2),
AMOUNT_NEW DECIMAL(11,2)
);
-- AFTER INSERT trigger: log every new order
CREATE OR REPLACE TRIGGER ORDLIB.TRIG_ORDMST_INS
AFTER INSERT ON ORDLIB.ORDMST
REFERENCING NEW ROW AS newrow
FOR EACH ROW
MODE DB2ROW
BEGIN
INSERT INTO APPLIB.ORD_AUDIT_LOG
(AUDIT_ACTION, ORD_NO, CUST_NO, STATUS_NEW, AMOUNT_NEW)
VALUES
('I', newrow.ORD_NO, newrow.CUST_NO, newrow.STATUS, newrow.ORD_AMOUNT);
END;
-- AFTER UPDATE trigger: log status and amount changes
CREATE OR REPLACE TRIGGER ORDLIB.TRIG_ORDMST_UPD
AFTER UPDATE OF STATUS, ORD_AMOUNT ON ORDLIB.ORDMST
REFERENCING OLD ROW AS oldrow NEW ROW AS newrow
FOR EACH ROW
MODE DB2ROW
BEGIN
INSERT INTO APPLIB.ORD_AUDIT_LOG
(AUDIT_ACTION, ORD_NO, CUST_NO, STATUS_OLD, STATUS_NEW, AMOUNT_OLD, AMOUNT_NEW)
VALUES
('U', newrow.ORD_NO, newrow.CUST_NO,
oldrow.STATUS, newrow.STATUS,
oldrow.ORD_AMOUNT, newrow.ORD_AMOUNT);
END;
-- AFTER DELETE trigger: log deleted orders with full snapshot
CREATE OR REPLACE TRIGGER ORDLIB.TRIG_ORDMST_DEL
AFTER DELETE ON ORDLIB.ORDMST
REFERENCING OLD ROW AS oldrow
FOR EACH ROW
MODE DB2ROW
BEGIN
INSERT INTO APPLIB.ORD_AUDIT_LOG
(AUDIT_ACTION, ORD_NO, CUST_NO, STATUS_OLD, AMOUNT_OLD)
VALUES
('D', oldrow.ORD_NO, oldrow.CUST_NO, oldrow.STATUS, oldrow.ORD_AMOUNT);
END;
BEFORE Trigger: Validation and Defaulting
A BEFORE trigger fires before the DML statement modifies the table. It can modify the NEW transition variable to alter the values being inserted or updated — this is the correct mechanism for computed columns, enforced defaults, or input normalisation that cannot be expressed as a column default or check constraint.
-- BEFORE INSERT trigger: normalise and validate a new order
CREATE OR REPLACE TRIGGER ORDLIB.TRIG_ORDMST_BFINS
BEFORE INSERT ON ORDLIB.ORDMST
REFERENCING NEW ROW AS newrow
FOR EACH ROW
MODE DB2ROW
BEGIN
-- Force the creation timestamp to the current time (override any application value)
SET newrow.CREATED_TS = CURRENT TIMESTAMP;
-- Normalise status to uppercase
SET newrow.STATUS = UPPER(TRIM(newrow.STATUS));
-- Default status to 'O' (Open) if not supplied
IF newrow.STATUS IS NULL OR newrow.STATUS = '' THEN
SET newrow.STATUS = 'O';
END IF;
-- Validate customer exists before inserting the order
IF NOT EXISTS (
SELECT 1 FROM SALESLIB.CUSTMST WHERE CUST_NO = newrow.CUST_NO
) THEN
-- SIGNAL raises an SQL error that aborts the INSERT
SIGNAL SQLSTATE '75001'
SET MESSAGE_TEXT = 'Customer does not exist: ' || TRIM(newrow.CUST_NO);
END IF;
END;
-- BEFORE UPDATE trigger: prevent status regression
-- (an order cannot move from 'S' Shipped back to 'O' Open)
CREATE OR REPLACE TRIGGER ORDLIB.TRIG_ORDMST_BFUPD
BEFORE UPDATE OF STATUS ON ORDLIB.ORDMST
REFERENCING OLD ROW AS oldrow NEW ROW AS newrow
FOR EACH ROW
MODE DB2ROW
WHEN (oldrow.STATUS = 'S' AND newrow.STATUS = 'O')
BEGIN
SIGNAL SQLSTATE '75002'
SET MESSAGE_TEXT = 'Cannot reopen a shipped order: ' || TRIM(newrow.ORD_NO);
END;
FOR EACH STATEMENT Triggers and Transition Tables
A FOR EACH ROW trigger fires once for every row affected by the DML statement. A FOR EACH STATEMENT trigger fires once for the entire statement, regardless of how many rows were affected. Statement-level triggers are more efficient for bulk operations; they use transition tables (OLD TABLE and NEW TABLE) to access the complete set of changed rows.
-- FOR EACH STATEMENT trigger: log a summary of a bulk status update
-- Rather than one audit row per order (potentially thousands), log one summary row
CREATE TABLE APPLIB.BULK_OP_LOG (
LOG_TS TIMESTAMP NOT NULL DEFAULT CURRENT TIMESTAMP,
OPERATION CHAR(1) NOT NULL,
TABLE_NAME VARCHAR(30) NOT NULL,
ROWS_CHANGED INT NOT NULL,
CHANGED_BY VARCHAR(18) NOT NULL DEFAULT CURRENT USER
);
CREATE OR REPLACE TRIGGER ORDLIB.TRIG_ORDMST_BULK_UPD
AFTER UPDATE ON ORDLIB.ORDMST
REFERENCING NEW TABLE AS newtable
FOR EACH STATEMENT
MODE DB2ROW
BEGIN
INSERT INTO APPLIB.BULK_OP_LOG (OPERATION, TABLE_NAME, ROWS_CHANGED)
SELECT 'U', 'ORDLIB.ORDMST', COUNT(*)
FROM newtable;
END;
INSTEAD OF Triggers on Views
An INSTEAD OF trigger fires in place of an INSERT, UPDATE, or DELETE on a view that would otherwise be non-updatable. This is the standard pattern for exposing a business-layer view to applications while routing writes through validation logic.
-- Non-updatable view joining orders and customers
CREATE OR REPLACE VIEW APPLIB.V_ORDER_SUMMARY AS
SELECT o.ORD_NO, o.CUST_NO, c.CUST_NAME,
o.STATUS, o.ORD_AMOUNT, o.ORD_DATE
FROM ORDLIB.ORDMST o
JOIN SALESLIB.CUSTMST c ON c.CUST_NO = o.CUST_NO;
-- INSTEAD OF INSERT: route inserts to ORDMST, ignore the CUST_NAME column
CREATE OR REPLACE TRIGGER APPLIB.TRIG_V_ORDER_SUMMARY_INS
INSTEAD OF INSERT ON APPLIB.V_ORDER_SUMMARY
REFERENCING NEW ROW AS newrow
FOR EACH ROW
MODE DB2ROW
BEGIN
INSERT INTO ORDLIB.ORDMST (ORD_NO, CUST_NO, STATUS, ORD_AMOUNT, ORD_DATE)
VALUES (newrow.ORD_NO, newrow.CUST_NO, newrow.STATUS,
newrow.ORD_AMOUNT, newrow.ORD_DATE);
END;
Managing Triggers: DROP, ALTER, and Display
-- List all triggers on a table
SELECT TRIGGER_NAME, EVENT_MANIPULATION, ACTION_TIMING,
ACTION_ORIENTATION, ENABLED
FROM QSYS2.SYSTRIGGERS
WHERE EVENT_OBJECT_TABLE = 'ORDMST'
AND EVENT_OBJECT_SCHEMA = 'ORDLIB'
ORDER BY ACTION_TIMING, EVENT_MANIPULATION;
-- Display trigger source (SQL text)
SELECT TRIGGER_TEXT
FROM QSYS2.SYSTRIGGERS
WHERE TRIGGER_SCHEMA = 'ORDLIB'
AND TRIGGER_NAME = 'TRIG_ORDMST_UPD';
-- Drop a trigger
DROP TRIGGER ORDLIB.TRIG_ORDMST_UPD;
-- Disable a trigger without dropping it (IBM i 7.4+)
ALTER TRIGGER ORDLIB.TRIG_ORDMST_UPD DISABLE;
-- Re-enable a disabled trigger
ALTER TRIGGER ORDLIB.TRIG_ORDMST_UPD ENABLE;
-- Drop all triggers on a table (useful during bulk data loads)
-- Drop them by name — there is no DROP ALL TRIGGERS shorthand
DROP TRIGGER ORDLIB.TRIG_ORDMST_INS;
DROP TRIGGER ORDLIB.TRIG_ORDMST_UPD;
DROP TRIGGER ORDLIB.TRIG_ORDMST_DEL;
DROP TRIGGER ORDLIB.TRIG_ORDMST_BFINS;
DROP TRIGGER ORDLIB.TRIG_ORDMST_BFUPD;
Trigger Best Practices on IBM i
- Keep trigger bodies short — triggers fire on every affected row; a trigger body that runs a complex multi-table query can severely impact INSERT/UPDATE/DELETE performance under load. Benchmark with realistic data volumes before deploying.
- Never issue COMMIT or ROLLBACK inside a trigger — the trigger executes within the calling statement’s UOW. A COMMIT inside a trigger commits changes the caller did not intend to commit. Use SIGNAL to abort the DML instead.
- Disable triggers during bulk data loads — use ALTER TRIGGER … DISABLE before loading large volumes of historical data, then re-enable and manually populate the audit table. This avoids millions of trigger firings for a one-time load.
- Use SIGNAL with application SQLSTATE values (75000–75999) — SQLSTATE values in the 75xxx range are reserved for application use. Returning a clear SQLSTATE and message text from a BEFORE trigger makes it easy for applications to identify and handle the specific validation failure.
- Test triggers under commitment control — trigger-generated audit rows participate in the calling statement’s UOW. If the UOW rolls back, the audit rows roll back too. If you need unconditional audit logging, use an autonomous transaction approach (write to a data queue and process separately).
Next post: IBM i save and restore operations — saving and restoring individual objects with SAVOBJ and RSTOBJ, saving entire libraries with SAVLIB and RSTLIB, performing full system saves with SAVSYS, running incremental saves with SAVCHGOBJ, creating and using save files with CRTSAVF, saving the IFS with SAVSAVFDTA, and designing a robust IBM i backup strategy using GO SAVE and BRMS in 2026.