Quick Summary — I gave IBM Bob a single detailed prompt describing a complete Student Admission Management System for IBM i. Using IBM i Developer Mode, powered by the Premium Package for i (PPI), IBM Bob planned the application, generated the IBM i backend, handled compilation and database setup, and built the web interface using Python Flask, HTML, CSS, and JavaScript in a single development session. What makes this story interesting is not just what got built, but how IBM Bob worked through the real IBM i platform constraints that came up along the way.

The Goal: A Full-Stack Admission System on IBM i

The application I had in mind was a Student Admission Management System. I wanted to build it as a complete application on IBM i, with Db2 for i and RPGLE handling the backend, Python Flask providing the REST API, and HTML, CSS, and JavaScript providing the browser interface.

I also wanted the application to include the kinds of features you would expect in a real admission system, such as adding and updating students, filtering and sorting records, pagination, soft-delete, and CSV export.

The web layer would connect to Db2 for i using the ibm_db_dbi Python driver. I wanted to see how IBM Bob would put all these pieces together and handle the IBM i-specific issues that came up during development.

IBM Bob Premium Package for i: Why This Works

Before getting into the development, it is worth explaining what made this application possible. The IBM i capabilities I used throughout the application came from Premium Package for i (PPI).

With IBM i Developer Mode active, IBM Bob could work directly with my IBM i environment through Code for IBM i. It was not just generating code for me to copy and run later. IBM Bob could create source members, execute CL commands, run SQL against Db2 for i, read compiler output, work with IFS files, and check the results directly on the system.

With IBM i Developer Mode active, IBM Bob can:

Without PPI, this would be a text generation exercise. With PPI, IBM Bob becomes a development partner that actually executes the work, catches the errors, and resolves them using knowledge of IBM i internals.

Also worth noting: IBM Bob also used IBM i Skills that encode platform-specific knowledge about module compilation sequences, binder source case sensitivity rules, CCSID constraints, journaling requirements, and Db2 for i SQL syntax.

The Prompt: One Detailed Message

I gave IBM Bob a single detailed prompt with the requirements for the entire application. I described the database schema, RPGLE architecture, Python Flask layer, IFS directory structure, and compilation requirements all in one message.

I also made a few IBM i-specific requirements clear. The SCHADMLIB library already existed on the system, so IBM Bob should not recreate it. All RPGLE and SQLRPGLE source had to be created as QSYS source members inside SCHADMLIB. I also specified that the RPGLE /copy directives should use QSYS member syntax rather than IFS paths. You can read the full prompt here.

The full prompt submitted to IBM Bob in IBM i Developer Mode
The single prompt submitted to IBM Bob in IBM i Developer Mode describing all application requirements

I was curious to see what IBM Bob would do with a prompt this long. Would it ask me to break the work into smaller pieces? Would it start with the database and wait for the next instruction? It did neither. IBM Bob first activated five IBM i Skills and then created a structured plan for the application.

The Build Plan: IBM Bob's 16-Phase Approach

Before starting the build, IBM Bob first checked the current state of SCHADMLIB using the QSYS2.OBJECT_STATISTICS SQL Service. It confirmed that the library was empty and then created a structured plan for the application.

IBM Bob's 16-phase todo list in IBM i Developer Mode
The structured 16-phase implementation plan generated before any source was written

What I liked about the plan was that IBM Bob put the steps in the right order. There were dependencies between several parts of the application, and those dependencies mattered on IBM i:

IBM Bob tracked every phase using the update_todo_list tool throughout the session, marking items complete as each phase succeeded and keeping the remaining work visible. It is a small detail, but it meant the entire build state was always clear throughout the session.

IBM i Skills: What IBM Bob Selected and Why

One of the things I wanted to understand during this build was how IBM Bob would use the IBM i Skills provided through Premium Package for i (PPI). IBM i Skills provide IBM i-specific knowledge that IBM Bob can use when working on a particular task. For this application, IBM Bob activated five Skills before generating the backend source:

Skill ActivatedUsed For
rpg-primer-basics Foundational IBM i RPG conventions — ctl-opt options, date formats, option flags
rpg-ile-understanding ILE concepts — module/service program architecture, activation groups, binding directories
rpg-free-format-fundamentals Fully free-format **free syntax, dcl-proc/end-proc, dcl-pi/end-pi
rpg-procedures-functions Exported procedure definitions, return types, likeds parameters, const qualifiers
rpg-embedded-sql SQLSTATE checking, GET DIAGNOSTICS ROW_COUNT, host variables, EXEC SQL in free-format RPG
IBM Bob activating five IBM i Skills at the start of the session
Five IBM i Skills activated before the backend source was generated

I found the combination interesting. IBM Bob did not just load the RPG Skills. It also activated the embedded SQL Skill because the backend used SQLRPGLE modules. It also needed the ILE and procedure Skills because those modules would eventually become part of a service program. That gave me a good indication that IBM Bob was looking at the application as a whole and selecting the IBM i knowledge it needed before starting the actual code generation.

Phase 1: QSYS Setup and IFS Directory Structure

The first step was setting up the places where the application source would live. IBM Bob created four source physical files in SCHADMLIB using the CRTSRCPF CL command. These source physical files had different roles. QRPGLESRC was for the RPGLE and SQLRPGLE source members, QRPGLEREF was for the copybook, QSRVSRC was for the service program binder source, and QBNDSRC was for the binding directory definition.

Source physical files created in SCHADMLIB using CRTSRCPF
Four source physical files created in SCHADMLIB using CRTSRCPF

IBM Bob then created the IFS directory structure for the web application using a single PASE shell command:

PASE — mkdir
mkdir -p /STUADMAPP/QSQLSRC \
          /STUADMAPP/WEBSRC/templates \
          /STUADMAPP/WEBSRC/static/css \
          /STUADMAPP/WEBSRC/static/js \
          /STUADMAPP/LOGS \
          /STUADMAPP/CONFIG

At this point, the application had two separate areas on IBM i. SCHADMLIB in QSYS was being used for the IBM i source and objects, while /STUADMAPP on the IFS was used for the Python, HTML, CSS, JavaScript, and SQL files. I liked this separation because it made the structure of the application clear from the beginning. The IBM i source stayed in QSYS, while the web and open-system files stayed on the IFS.

Phase 2: The Database Layer and a Real DDL Challenge

The database design was more than just creating a table. The STUDENTS table needed an identity column for the student ID, validation rules for the pincode and contact number, a status field defaulting to 'A', and timestamps for tracking changes. I also needed a separate audit table to record changes to student records.

IBM Bob created the DDL script in /STUADMAPP/QSQLSRC/STUADM_DDL.sql and started executing it. This is where the session got interesting — three consecutive DDL execution attempts failed, each for a different IBM i-specific reason, and IBM Bob worked through all three systematically.

The Database Layer DDL execution in IBM Bob
IBM Bob executing the DDL script for the STUDENTS and STUD_AUDIT tables in SCHADMLIB

DDL Issue 1 — RCDFMT Conflicts with GENERATED AS IDENTITY

Symptom: First CREATE TABLE attempt included RCDFMT STUDENTSF alongside GENERATED ALWAYS AS IDENTITY. Db2 for i does not allow these together.

IBM Bob's immediate response was to remove RCDFMT entirely. That is the correct fix — the record format name clause is a legacy DDS-era feature and has no meaningful role in SQL-created tables.

DDL Issue 2 — NO ORDER Not Supported in Db2 for i

Symptom: SQL0574 — column attribute not valid. The identity column options included NO MAXVALUE NO CYCLE NO ORDER CACHE 20. The NO ORDER keyword is not recognized by Db2 for i.

IBM Bob simplified the identity options down to the minimum that Db2 for i actually supports: (START WITH 1 INCREMENT BY 1). Clean, correct, no platform-specific keywords.

DDL Issue 3 — CCSID 65535 Blocks DEFAULT USER

Symptom: SQL0574 persisted. The root cause turned out to be the job CCSID. The PASE job running the SQL had CCSID 65535 — a hex CCSID that cannot translate SQL special register names like DEFAULT USER.

This is one of those IBM i issues that is genuinely tricky to diagnose. DEFAULT USER works perfectly in an interactive SQL session or in an RPG embedded SQL statement. But when the job CCSID is 65535, the SQL precompiler cannot resolve the special register name, and you get SQL0574 with no obvious clue about what is wrong.

Why this matters: CCSID 65535 is common in PASE jobs and SSH sessions on IBM i. It is one of the first things you learn to watch for when scripting DDL or SQL from the PASE environment. IBM Bob identified it from the error pattern and applied the right workaround.

IBM Bob resolved this in two ways. For the DDL, it replaced DEFAULT USER with DEFAULT 'SYSTEM', which the CCSID 65535 job can handle without issue. For executing the DDL, it switched from the direct execute_sql_statement tool to a Python ibm_db_dbi script using a local connection — ibm_db_dbi.connect() with no arguments creates a connection that runs in a standard CCSID context rather than inheriting the PASE job's CCSID.

Before — fails on CCSID 65535
-  UPDATED_BY VARCHAR(10) NOT NULL
-             DEFAULT USER,
After — works on any CCSID
+  UPDATED_BY VARCHAR(10) NOT NULL
+             DEFAULT 'SYSTEM',

The actual user value is set explicitly at DML time in every INSERT and UPDATE statement — UPDATED_BY = ? passed as a parameter from the application layer. The column default of 'SYSTEM' is only a safety fallback. This is actually a cleaner design, because it means the audit column is always populated by the application's known user context rather than a database-level special register.

Tables created successfully. SCHADMLIB.STUDENTS (17 columns, identity PK, 3 CHECK constraints) and SCHADMLIB.STUD_AUDIT (6 columns) — both created. Three indexes added: on STATUS, on STANDARD/SECTION, and on STUD_NAME.

Phases 3–8: The RPGLE Backend

The Shared Copybook: STUDINC

Before any module could be written, IBM Bob created the shared copybook in SCHADMLIB/QRPGLEREF as member STUDINC. The copybook is the backbone of the entire backend — it defines the StudDS data structure (all 17 table columns mapped to RPG variables), the ResultDS return structure, constants for return codes and audit operation types, and forward prototypes for all eight exported procedures.

Every module uses /copy SCHADMLIB/QRPGLEREF,STUDINC — the QSYS member syntax that the prompt specifically required. IBM Bob used that syntax consistently throughout all four modules without deviation.

STUDINC copybook member in SCHADMLIB/QRPGLEREF
The shared copybook containing StudDS, ResultDS, constants, and procedure prototypes

The ResultDS Pattern: Consistent Error Handling

Every exported procedure returns a ResultDS data structure. This is a deliberate architectural decision. Instead of checking SQLSTATE in the calling program, the caller simply checks result.ReturnCode. The module handles all the SQL error checking internally and populates the result with a human-readable message and the raw SQLCODE/SQLSTATE for logging. It is a clean pattern that scales — the Python layer, when it calls through to this service program in future, gets a consistent interface regardless of which operation failed.

The Four Modules

ModuleSource MemberExported ProceduresKey Logic
STUINSERT QRPGLESRC/STUINSERT AddStudent, ValidatePincode, ValidateContact, WriteAudit Validates pincode and contact, inserts with IDENTITY_VAL_LOCAL(), writes to STUD_AUDIT on success
STUSELECT QRPGLESRC/STUSELECT GetStudent, GetStudents Single-row SELECT INTO; multi-row count with token-based filter parsing (NAME:, STD:, SEC:)
STUUPDATE QRPGLESRC/STUUPDATE UpdateStudent Re-validates all fields, uses GET DIAGNOSTICS ROW_COUNT to detect zero-match updates
STUDELETE QRPGLESRC/STUDELETE DeleteStudent Two-step: confirms active record exists first, then UPDATE STATUS = 'I' — no physical DELETE ever runs

The soft-delete pattern in STUDELETE is worth highlighting because IBM Bob implemented it with the right two-step approach. It first does a SELECT STUD_NAME INTO :studName FROM STUDENTS WHERE STUDENT_ID = :pStudentId AND STATUS = 'A'. If that returns SQLCODE = +100 (not found or already inactive), it returns RC_NOTFOUND immediately without touching the data. Only if the record is confirmed active does it proceed with the status update. The audit write then captures the student's name in the log before that information is no longer easily retrievable.

STUDELETE module — two-step soft delete with pre-validation SELECT
DeleteStudent procedure confirming active status before issuing the soft-delete UPDATE

The Binder Source: Getting the Symbol Case Right

IBM Bob initially wrote the binder source with mixed-case export symbols: EXPORT SYMBOL('AddStudent'). It then used DSPMOD MODULE(SCHADMLIB/STUINSERT) to inspect the actual exported symbol names from the compiled module. What came back was ADDSTUDENT, not AddStudent.

Why this happens: In free-format RPGLE, when a dcl-proc is declared with export and no explicit extproc qualifier, the compiler exports the symbol in uppercase. This is different from the mixed-case behavior you get with extproc(*dclcase). The binder source symbols must match the compiled export exactly, including case. Unquoted symbols in binder source are uppercased by the binder. Quoted symbols preserve case. The combination that actually works here is uppercase quoted strings.
Before — wrong case
-  EXPORT SYMBOL('AddStudent')
-  EXPORT SYMBOL('GetStudent')
-  EXPORT SYMBOL('UpdateStudent')
After — matches compiled exports
+  EXPORT SYMBOL('ADDSTUDENT')
+  EXPORT SYMBOL('GETSTUDENT')
+  EXPORT SYMBOL('UPDATESTUDENT')

Phase 9: Compilation: All Four Modules at Severity 00

With all source members in place and the binder source corrected, IBM Bob compiled each module using CRTSQLRPGI with OBJTYPE(*MODULE). The DFTRDBCOL(SCHADMLIB) parameter is important here — it tells the SQL precompiler to use SCHADMLIB as the default collection for unqualified SQL table references, which means FROM STUDENTS in the RPGLE source correctly resolves to SCHADMLIB.STUDENTS at compile time.

All four modules — severity 00. STUINSERT · STUSELECT · STUUPDATE · STUDELETE — all compiled clean. Zero errors, zero warnings.
All four RPGLE modules compiled at severity 00 in SCHADMLIB
Compilation results for all four SQLRPGLE modules — severity 00 across the board

Phase 10: Service Program and Binding Directory

With all four modules compiled, IBM Bob created the STUDSRVP service program and bound the four modules together. The service program exposes eight procedures: ADDSTUDENT, GETSTUDENT, GETSTUDENTS, UPDATESTUDENT, DELETESTUDENT, VALIDATEPINCODE, VALIDATECONTACT, and WRITEAUDIT. The signature string STUADM_V1R0M0 is the versioning hook — if I add new procedures in the future, they can be added at the end of the binder source while keeping the existing interface compatible with programs that are already using the service program.

Service Program and Binding Directory created in SCHADMLIB
STUDSRVP service program and STUDBNDDIR binding directory created successfully in SCHADMLIB

The ACTGRP(*CALLER) setting means the service program runs in its caller's activation group rather than creating its own. For this application, where the service program is called from Python via a SQL stored procedure or directly via XMLSERVICE, this is the right setting — it avoids creating unnecessary activation groups and keeps resource management straightforward.

The Journaling Issue: Understanding IBM i Journaling Behavior

After the database tables were created and the RPGLE backend was compiled, IBM Bob tried to insert sample data using the Python ibm_db_dbi driver. The first INSERT failed with SQL0-7008 — a file not journaled for commitment control error.

This one catches developers who are new to Db2 for i. When you connect through ODBC or ibm_db_dbi, the driver typically uses connection-level commitment control. Db2 for i requires tables to be enrolled in a journal before they can participate in committed transactions. Tables created via DDL exist in the database but are not automatically journaled — you have to enroll them explicitly.

IBM Bob first checked whether a journal already existed in SCHADMLIB using WRKOBJ OBJ(SCHADMLIB/*ALL) OBJTYPE(*JRN). There was none. It then created the full journal infrastructure in sequence:

CL — Journal Setup
QSYS/CRTJRNRCV JRNRCV(SCHADMLIB/SCHADMRCV)
    THRESHOLD(100000)
    TEXT('Student Admission Journal Receiver')
    AUT(*EXCLUDE)

QSYS/CRTJRN JRN(SCHADMLIB/SCHADMJRN)
    JRNRCV(SCHADMLIB/SCHADMRCV)
    MNGRCV(*SYSTEM) DLTRCV(*YES)
    TEXT('Student Admission Journal')
    AUT(*EXCLUDE)

QSYS/STRJRNPF FILE(SCHADMLIB/STUDENTS SCHADMLIB/STUD_AUDIT)
    JRN(SCHADMLIB/SCHADMJRN)
MNGRCV(*SYSTEM) and DLTRCV(*YES): IBM Bob used MNGRCV(*SYSTEM) so IBM i manages journal receiver chaining automatically, and DLTRCV(*YES) to allow automatic deletion of detached receivers once they are no longer needed. For a development environment, this keeps the journal from consuming disk space indefinitely. In production, you would typically want to manage receiver retention policy more explicitly.

The Python Driver Numeric Parameter Issue

After journaling was in place, IBM Bob loaded 10 sample students using the Python ibm_db_dbi driver. Seven inserted successfully on the first attempt. Three did not. The error was not a CHECK constraint violation — the pincode and contact values were correct. The root cause was a Python driver behavior specific to this IBM i environment.

When passing large integer values (specifically CONTACT_NO = 9876543210) through Python's ? parameterized query binding, the ibm_db_dbi driver failed to map the Python int to NUMERIC(10,0). The same values worked perfectly when passed as literal strings.

Before — int fails for large numerics
-  ('Priya Patel', 12, ...,
-    4110001, 9123456780)
-  # pincode=int, contact=int
-  # → driver type mapping fails
After — strings work correctly
+  ('Priya Patel', 12, ...,
+    '4110001', '9123456780')
+  # pincode=str, contact=str
+  # → Db2 casts string → NUMERIC

IBM Bob also patched app.py to use the same string-passing approach for the Flask API endpoints — so the production INSERT and UPDATE operations would work correctly for all users. A small fix, but an important one to get right before the application goes live.

Phases 11–14: The Python Flask Web Application

With the database layer solid and the RPGLE backend compiled, IBM Bob built the entire web tier simultaneously. Four write_stream_file calls went out in parallel: db.py, requirements.txt, config.py, and app.py. These landed in /STUADMAPP/WEBSRC on the IFS.

The REST API: Five Endpoints

MethodEndpointWhat It Does
POST/api/studentsValidate → INSERT → IDENTITY_VAL_LOCAL() → audit → return new ID
GET/api/studentsFilter by name/standard/section/status + LIMIT/OFFSET pagination → JSON
GET/api/students/<id>Single student row → JSON with serialized dates
PUT/api/students/<id>Validate → UPDATE → rowcount check → audit → return result
DELETE/api/students/<id>Confirm active → UPDATE STATUS='I' → audit → no physical delete
GET/api/statsTotal / active / inactive counts + GROUP BY STANDARD for dashboard

The _validate_student helper function in app.py mirrors the RPGLE validation logic — same pincode range check, same contact number range check, same date format requirement. Dual validation is intentional: client-side JavaScript catches format errors before the request is sent; server-side Python validates again before touching the database; and the database CHECK constraints act as a final guard. A record that violates the constraints cannot be inserted regardless of how the request was constructed.

app.py REST API endpoint for POST /api/students with server-side validation
The api_add_student endpoint showing validation, INSERT, identity retrieval, and audit write

The db.py Connection Helper

The database connection uses ibm_db_dbi.connect() with no arguments. This is the same local connection pattern that worked reliably during the DDL execution. The fetchall_dict helper converts the cursor rows into dictionaries using the column names from cursor.description. This makes it easier for the API endpoints to return the database results as JSON.

db.py Connection Helper using ibm_db_dbi local connection
The db.py connection helper using ibm_db_dbi.connect() with the fetchall_dict utility

HTML Templates: One Form, Two Modes

The student form template (student_form.html) is shared between Add and Edit operations. Flask passes a MODE value, either add or edit, and for edit operations it also passes the STUDENT_ID. The JavaScript uses these values to decide which API endpoint to call when the form is submitted. For a new student, it calls POST /api/students. For an existing student, it calls PUT /api/students/<id>. I liked this approach because there is only one form to maintain instead of having two almost identical templates.

student_form.html rendered in add mode — the full admission data entry form
The shared student form template with personal, academic, family, and contact sections

JavaScript: Real-Time Validation and CSV Export

The student_form.js file drives the form validation using a field definition map (FIELDS) that describes each field's type and constraints. The validation logic covers required fields, age range, date format, pincode regex (/^\d{7}$/), and contact regex (/^\d{10}$/). Inline red error messages appear next to each field without a page reload.

The student list page (students_list.js) includes client-side column sorting on the cached data, filter parameter building with URLSearchParams, and a CSV export that generates a downloadable file directly in the browser from the current page's data — no server round-trip needed for the export.

Final Results: 16 Phases, All Complete

After resolving the DDL CCSID issues, the journaling requirement, the binder symbol case problem, and the Python driver numeric parameter behavior, the full build reached completion.

IBM Bob's todo list with all 16 phases marked complete
All 16 phases marked complete in IBM Bob's build tracker
All 16 phases complete. 4 RPGLE modules at severity 00. Full-stack application running on IBM i. From QSYS source members through compiled service program to a live Flask web application with 10 sample students loaded — all built in a single development session using IBM i Developer Mode in IBM Bob Premium Package for i (PPI).

The Application Running

Once Flask was started with python3 -m flask run --host=0.0.0.0 --port=5000, the application was accessible from a browser at http://p10adt2.rch.stglabs.ibm.com:5000/.

Student Admission System dashboard showing live stats
Dashboard at / — live counts pulled from /api/stats, students by standard breakdown
All Students list page — 10 rows, filter bar, sort headers, Deactivate buttons
The student list with filter controls, sortable columns, and per-row Edit and Deactivate actions
Add New Student form — all sections filled, real-time validation visible
The Add Student form showing the 7-digit pincode validation feedback

Productivity Comparison

The table below compares the time I would typically expect to spend developing this application manually with the time it took using IBM i Developer Mode in IBM Bob Premium Package for i (PPI).

TaskManuallyWith IBM i Developer Mode
QSYS source physical files + IFS directory structure 30 min ~2 minutes
SQL DDL — table, indexes, audit table, labels, grants 2–3 hours ~8 minutes (including 3 DDL fix cycles)
Journal setup (receiver, journal, STRJRNPF) 20 min Included — IBM Bob identified the need from the SQL0-7008 error
RPGLE copybook + 4 SQLRPGLE service modules 2–3 days ~20 minutes
Binder source + CRTSRVPGM + CRTBNDDIR 1–2 hours ~5 minutes (including binder symbol case fix)
Python Flask app (5 REST endpoints, validation, audit) 1–2 days ~15 minutes
HTML templates (4 files), CSS, JavaScript (3 files) 2–3 days ~15 minutes
Diagnosing CCSID 65535, journaling, driver numeric issue 1–2 days ~20 minutes across all fix cycles
Total estimated ~2–3 weeks < 2 hours

The troubleshooting time stands out most. CCSID 65535 blocking DEFAULT USER, tables requiring journaling before DML, and the Python driver's numeric parameter handling for large integers — these are all real IBM i platform behaviors that are not well-documented in one place. Finding and resolving them manually would typically involve reading multiple IBM documentation pages, Stack Overflow threads, and forum posts across multiple sessions. IBM Bob diagnosed and fixed all three in the same session they appeared, without interrupting the build flow.

What Made This Possible

Every IBM i capability used in this build came through the Premium Package for i (PPI). That included creating QSYS source members, executing CL commands, running SQL against QSYS2 SQL Services to verify objects, writing files to the IFS through PASE, reading compiler output to diagnose failures, and using IBM i Skills for SQLRPGLE, binder source, CCSID, journaling, and Db2 for i DDL. Without PPI, this would have been mainly a code generation exercise. With IBM i Developer Mode and Premium Package for i (PPI), IBM Bob worked as a development partner that could build, compile, validate, troubleshoot, and verify the application directly on the IBM i system.

Watch It in Action

A full walkthrough of the Student Admission Management System running live on IBM i, built using IBM i Developer Mode in IBM Bob Premium Package for i (PPI).

Next Steps

If you want to try IBM Bob and IBM Bob Premium Package for i (PPI) yourself, here are the resources to get started: