Blog › ICP guides
Progress OpenEdge developer on retainer: transaction scope, ABL 4GL, ProDB record locking, and Progress OpenEdge on monthly retainer
October 3, 2026 · ~14 min read
A Progress OpenEdge developer was maintaining an inventory batch update application written in Progress ABL (Advanced Business Language, also called Progress 4GL) for a European distribution company. The application ran nightly to update inventory levels across 12 warehouse locations from purchase order receipt data stored in the ProDB database. The developer had written the update loop to process each warehouse location’s records in sequence, using FOR EACH warehouse_inventory WHERE wh_id = loc_id: and ASSIGN inv_qty = inv_qty + received_qty. When a network interruption cut the batch job at 11:47 PM — after 8 of 12 warehouse locations had been processed — the job aborted. The next morning, 3 inventory records in the partially-processed locations were found in inconsistent states: some records within a location were updated, others within the same location were not.
The root cause was the absence of an explicit DO TRANSACTION: block. Progress ABL wraps each top-level database operation in an implicit transaction if no explicit transaction block is present. This means each ASSIGN statement inside the FOR EACH loop committed immediately and independently — each individual field update was its own transaction. When the network interruption caused the job to abort mid-location, the records that had already been assigned (each committed as its own transaction) could not be rolled back, while the records that had not yet been reached remained at their prior values. Within a single warehouse location, some inventory records showed the updated quantity and others showed the pre-update quantity. The 3 inconsistent records were in Location 9, the partially-processed location at the time of the interruption.
The fix was to wrap the inner loop for each warehouse location in an explicit DO TRANSACTION: block: DO TRANSACTION: FOR EACH warehouse_inventory WHERE wh_id = loc_id: ASSIGN inv_qty = inv_qty + received_qty. END. END. The explicit DO TRANSACTION: establishes an application-level transaction boundary: all ASSIGN statements within the block are buffered as a single unit of work. If an error, network interruption, or UNDO condition terminates the block before END., Progress ABL rolls back all buffered changes to the state at DO TRANSACTION:. If the block completes normally, all changes are committed atomically. After the fix, a network interruption would at worst cause the entire current warehouse location’s update to roll back — the location would remain fully at the prior state, not partially updated — and the batch could be restarted for that location without producing inconsistent records. Three inconsistent inventory records: 3 → 0 after restructuring. The investigation — identifying the 3 inconsistent records, tracing the inconsistency to the partial commit within one warehouse location, understanding ABL’s implicit transaction model, and confirming the fix via a test run with simulated network interruption — was 3.5 hours.
Progress OpenEdge, ABL architecture, and the implicit vs explicit transaction model
Progress Software Corporation developed the Progress 4GL and Progress database in the early 1980s as a unified application development and database platform. Progress OpenEdge is the current platform designation (versions 11.x and 12.x are in widespread production deployment), encompassing the ABL language (Advanced Business Language, also written as OpenEdge ABL and still commonly called Progress 4GL), the ProDB database engine (also called the Progress database or OpenEdge RDBMS — a relational database with some object extensions), the Progress Application Server for OpenEdge (PAS for OpenEdge, the successor to the legacy AppServer for multi-tier deployment), Progress Developer Studio for Eclipse (the primary IDE), and OpenEdge Management and Management Reporter for operational monitoring. Progress Data Server connects OpenEdge applications to external relational databases such as Microsoft SQL Server and Oracle, allowing ABL programs to run DML against non-ProDB backends. WebSpeed and PAS for OpenEdge provide web and REST deployment of ABL procedures. ABL source files use the extension .p for procedures and .cls for class-based object-oriented ABL. Compiled r-code uses the .r extension. ABL compilation is triggered by the COMPILE statement or by the Progress Developer Studio IDE. The PROPATH environment variable functions analogously to a Java classpath: it specifies the directory sequence searched for .r r-code files when a procedure is called by name. Deployment modes include batch (a .p procedure run directly), client-server (via AppServer or PAS for OpenEdge), and web (REST via OpenEdge REST Adapter or WebSpeed).
The ABL transaction model has two operating modes, and the distinction between them is the source of the most dangerous class of Progress retainer bugs. In the implicit transaction mode — which applies whenever no explicit transaction block is active — ABL wraps each top-level DML operation (FIND, FOR EACH with ASSIGN or DELETE, CREATE, DELETE) in its own auto-committed transaction. Each individual DML operation completes and commits independently. There is no buffering: each ASSIGN is atomic with respect to itself, but not atomic with respect to other ASSIGN statements in the same loop iteration or the same logical group of records. When a batch job processes a group of records that should be treated as a single atomic unit of work — all records in a warehouse location, all line items on an order, all entries in an account period — and that group is processed using a FOR EACH loop without a surrounding DO TRANSACTION: block, each record’s update commits the moment the ASSIGN executes. If the job is interrupted partway through the group, the records already processed stay committed and the remaining records stay at their prior values. The group is in a split state.
The explicit transaction mode is activated by a DO TRANSACTION: block. All DML inside the block is buffered as a single unit of work. The transaction commits atomically when the block’s END. is reached normally. ABL provides three transaction control flow statements for use inside a DO TRANSACTION: block: UNDO, LEAVE rolls back all changes since the start of the DO TRANSACTION: block and exits the block; UNDO, RETRY rolls back all changes and re-executes the block from its start (used for lock-retry logic); and UNDO, RETURN ERROR rolls back all changes and returns an error condition to the calling context. UNDO without a DO TRANSACTION: block undoes only the current implicit statement-level transaction, which is already committed before the next statement executes. Nested DO TRANSACTION: blocks function as sub-transactions (savepoints): an UNDO inside an inner block rolls back to the start of that inner block, not to the start of the outer block. The outer transaction remains active and can continue or commit the remaining work. ProDB record locking interacts with transaction scope: SHARE-LOCK is the default for FOR EACH without an EXCLUSIVE-LOCK or NO-LOCK qualifier; it allows concurrent readers but prevents lock upgrades if another session also holds a SHARE-LOCK on the same record. EXCLUSIVE-LOCK prevents all concurrent access — reads and writes — and is required for ASSIGN, CREATE, and DELETE operations. NO-LOCK provides read-only access without any locking. The RELEASE statement explicitly releases a record lock before the transaction ends. Lock timeout and the NO-WAIT option on FIND and FOR EACH control behavior when a lock cannot be immediately acquired.
ProDB schema and ABL DML follow a consistent vocabulary. DEFINE VARIABLE declares typed variables; ABL supports CHARACTER, INTEGER, INT64, DECIMAL, LOGICAL, DATE, DATETIME, DATETIME-TZ, and RECID types. FIND FIRST, FIND LAST, FIND NEXT, and FIND PREV retrieve a single record matching a WHERE condition. FOR EACH table WHERE condition BY field: … END. iterates records in sorted order. CREATE table. inserts a new row. ASSIGN field = value. updates one or more fields; multiple fields can be updated in a single ASSIGN statement by chaining assignments separated by spaces: ASSIGN field1 = val1 field2 = val2. DELETE table. removes the current record. The AVAILABLE(table) function returns TRUE if a record of the named table is currently in scope — that is, if a prior FIND or FOR EACH has placed a record of that table in the ABL record buffer and no subsequent record change has cleared it. CAN-FIND(table WHERE condition) tests whether a matching record exists without retrieving it into the record buffer. Error handling uses ON ERROR UNDO, LEAVE as a default error block; MESSAGE ... VIEW-AS ALERT-BOX for interactive alerting; and ERROR-STATUS:ERROR (LOGICAL) plus ERROR-STATUS:GET-MESSAGE(n) for programmatic error inspection. Class-based ABL uses structured CATCH blocks with typed exception classes, most commonly Progress.Lang.AppError for application-level errors and Progress.Lang.SysError for system-level errors. The OpenEdge Business Rules framework provides a rules engine layer over ABL. AppServer stateless vs stateful connection model significantly affects transaction scope: in a stateless AppServer (or stateless PAS for OpenEdge) connection, all in-scope ProDB records and all ABL state are reset between procedure calls; a DO TRANSACTION: block cannot span two separate AppServer requests on a stateless connection.
Typical Progress OpenEdge retainer work and what it looks like in a work log
The implicit transaction partial commit bug is the most invisible category of Progress OpenEdge retainer work. The opening scenario is representative: a FOR EACH loop iterates over a ProDB table and executes ASSIGN inside the loop body, with no surrounding DO TRANSACTION: block. In test environments, batch jobs complete without interruption. Each ASSIGN commits individually, the job finishes, and all records are in the correct final state. The absence of a DO TRANSACTION: block produces no error, no warning, no performance difference, and no visible behavioral difference in a non-interrupted run. The bug only manifests when the job is interrupted mid-run by a network failure, an OS-level job timeout, an operator kill, or a power event. At that point, the records within the partially-processed logical group are split: some committed, some not. Work log entry: “INVBATCH.p: FOR EACH wh_inventory ASSIGN without DO TRANSACTION:; implicit per-statement commit; network interrupt at Location 9 produced 3 inconsistent records within location (some updated, others not); wrapped inner location loop in DO TRANSACTION: ... END.; added ON ERROR UNDO, LEAVE with error log; inconsistent records: 3 → 0; 3.5h.”
FIND without AVAILABLE check is the second most common Progress OpenEdge retainer pattern. The developer writes FIND FIRST customer WHERE cust_id = input_id. then immediately follows with ASSIGN customer.balance = customer.balance + payment. without checking IF AVAILABLE(customer) THEN. When input_id does not match any customer record, the FIND sets ERROR-STATUS:ERROR to TRUE but does not raise an immediate visible exception. The subsequent ASSIGN statement, however, raises a runtime error: “Record not available” — because no record of the customer table is in scope. This is not a silent wrong update (unlike some platforms where a missing guard produces a stale record update); it is a runtime abort. The bug is that no error handler was present, so the batch aborted entirely on the first missing customer rather than logging the missing customer and continuing to the next payment. Four customer payments went unapplied because the batch aborted before reaching them. Fix: IF AVAILABLE(customer) THEN ASSIGN customer.balance = customer.balance + payment. ELSE /* log missing customer */. Work log entry: “PAYBATCH.p: FIND FIRST customer no AVAILABLE check; runtime error on missing cust_id; 4 payments not applied; added IF AVAILABLE(customer) guard + error log; unapplied payments: 4 → 0; 2h.”
SHARE-LOCK vs EXCLUSIVE-LOCK contention is the third common Progress OpenEdge retainer category. The developer writes FOR EACH order WHERE status = ‘PENDING’: with the default SHARE-LOCK, then inside the loop executes ASSIGN order.status = ‘PROCESSING’. ABL will attempt to automatically upgrade the SHARE-LOCK to an EXCLUSIVE-LOCK when the ASSIGN executes — but this upgrade succeeds only if no other session currently holds a SHARE-LOCK on the same record. In a peak-hour concurrent environment, another session holds a SHARE-LOCK on the same order record (perhaps reading it for a status report or a web request). The lock upgrade fails. ABL retries. If retry logic is not present and NO-WAIT is not specified, the batch waits indefinitely (or until the ProDB lock timeout). If NO-WAIT is specified, the ASSIGN fails immediately with a lock-conflict error. Either way, the batch does not proceed normally. Eight lock timeout retries occurred before the batch was manually killed. Fix: change FOR EACH order WHERE status = ‘PENDING’ EXCLUSIVE-LOCK: to acquire the exclusive lock at the start of the loop iteration, before any other session can acquire a SHARE-LOCK on the same record during this loop’s processing window. Lock wait: 8 timeout retries → 0; 2.5h. The broader principle is that any FOR EACH loop body that contains ASSIGN or DELETE should use EXCLUSIVE-LOCK from the start — the SHARE-LOCK default is appropriate only for read-only iterations.
Progress OpenEdge developer retainer rates span a wide range by experience level. Entry-level Progress ABL developers typically bill at $70–$125 per hour. Mid-level Progress OpenEdge programmers with AppServer or PAS deployment experience and ProDB performance knowledge typically bill at $105–$185 per hour. Senior Progress OpenEdge developers with deep ABL transaction model knowledge, ProDB schema management experience, and legacy modernization capability typically bill at $150–$275 per hour. Monthly retainer engagements range from $1,800–$3,200/mo for advisory (15–22 hours) to $2,500–$5,800/mo for active maintenance.
Track Progress OpenEdge developer retainer hours without the status emails
When a 3.5-hour investigation traces three inconsistent inventory records to ABL’s implicit per-statement transaction model — each ASSIGN statement committed independently rather than as an atomic unit — the work log must name the program, the ProDB file iterated, the implicit vs explicit transaction distinction, and the inconsistent-record count before and after the DO TRANSACTION: block fix. HourTab gives your Progress OpenEdge retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log naming the ABL program, the transaction scope issue, and the fix. No client login. No status emails. CSV in, URL out.
How HourTab tracks Progress OpenEdge retainer hours
Progress ABL retainer work is invisible by the same mechanism that makes implicit transaction scope dangerous: in development and test environments, batch jobs are rarely interrupted mid-run, so the partial-commit behavior never appears. The implicit per-statement transaction model means each ASSIGN looks atomic in isolation — the developer verified that each individual ASSIGN updated the correct record, the correct quantity, the correct field. The batch ran end-to-end in the test environment and produced the correct final state for all records. The inconsistency only appears in production when an external interruption (network failure, OS signal, power event) terminates the job between ASSIGN statements within a group of records that should be treated atomically. There is no Progress-level error message, no stack trace, and no session log entry indicating that a partial commit occurred — the interrupted ASSIGN statements simply never executed, and the committed ones remain committed. The developer must reconstruct the transaction model from the ABL specification and the record state diffs to identify what DO TRANSACTION: block was missing and precisely which records fell on each side of the interruption boundary.
The work log needs to name the mechanism: which ABL program, which ProDB file and FOR EACH loop, which ASSIGN statements executed without a DO TRANSACTION: wrapper, what the implicit commit model means for that loop structure (each ASSIGN committed independently as its own auto-transaction), what the interruption scenario was (network failure at Location 9, after 8 of 12 warehouse locations had been processed), and what the inconsistent-record count was before and after the DO TRANSACTION: restructuring (3 → 0). A log entry that says “fixed batch transaction bug, 3.5h” is not auditable. A log entry that names the program, the FOR EACH loop on wh_inventory, the missing DO TRANSACTION: block, the implicit per-statement commit behavior, the interruption location, and the 3 inconsistent records that were restored is auditable and defensible. HourTab gives Progress OpenEdge developers a public retainer-hours URL they send to clients — European distribution companies, manufacturing firms, and financial institutions maintaining Progress ABL and ProDB applications built in the 1990s and 2000s. Comparative context: Progress ABL retainer work has structural overlap with adjacent enterprise batch platforms. Natural retainers cover Adabas transaction boundary bugs for European enterprise mainframe platforms — both are batch update jobs on legacy European enterprise platforms where partial commits produce invisible wrong states that only appear in production under interruption conditions. RPG retainers cover IBM i batch indicator-overwrite bugs that are also invisible in test and only visible in specific production data conditions.
FAQ: Progress OpenEdge developer retainers
What does a Progress OpenEdge developer on retainer typically do?
A Progress OpenEdge developer on monthly retainer covers transaction boundary audit (reviewing all FOR EACH and FIND loops that contain ASSIGN, CREATE, or DELETE to confirm explicit DO TRANSACTION: blocks are present where atomicity is required; confirming ON ERROR UNDO, LEAVE is present to handle partial errors; reviewing nested DO TRANSACTION: savepoint usage for sub-transaction rollback); FIND and AVAILABLE pattern review (auditing all FIND statements to confirm AVAILABLE() checks are present before ASSIGN or DELETE; adding missing checks and error logging for unmatched records); lock type analysis (reviewing FOR EACH and FIND calls for SHARE-LOCK vs EXCLUSIVE-LOCK correctness — EXCLUSIVE-LOCK should be used whenever the loop body contains ASSIGN or DELETE; auditing lock timeout settings and retry logic); PAS connection state (confirming stateless AppServer procedures do not rely on ProDB state or in-scope records surviving across AppServer requests — state is reset between calls on stateless connections); ABL OO class and error propagation (reviewing CATCH blocks in class-based ABL to confirm Progress.Lang.AppError is caught and re-thrown correctly, and that transaction rollback occurs on exception).
What Progress ABL work is most commonly underlogged?
Implicit transaction partial commit bugs are the most systematically underlogged Progress OpenEdge retainer work. The pattern: no explicit DO TRANSACTION: block; ABL’s implicit per-statement commits mean each ASSIGN in a FOR EACH loop is its own committed transaction; a batch interruption (network failure, OS signal, power event) leaves records in a partially-updated state within one logical group — some records within the same warehouse location are updated, others are not; the symptom is invisible in testing since test runs complete without interruption; it only appears in production under network failures or job timeouts. The 3 to 6 hour investigation produces no error message, no stack trace, and no call log showing the partial commit — the developer must reconstruct the implicit transaction model from the ABL specification and the record state diffs to identify what DO TRANSACTION: block was missing. The work log must name the program, the ProDB file iterated, the implicit vs explicit transaction distinction, and the inconsistent-record count before and after the fix.
What are typical Progress OpenEdge developer retainer rates?
Entry-level Progress OpenEdge developers with experience in basic ABL programming, ProDB DML (FOR EACH, FIND, ASSIGN, CREATE, DELETE), and standard batch program structure typically bill at $70 to $125 per hour. Mid-level Progress ABL programmers with experience in complex batch program design, ProDB performance optimization (index selection, SHARE-LOCK vs EXCLUSIVE-LOCK, record buffer management), and AppServer or PAS multi-tier deployment typically bill at $105 to $185 per hour. Senior Progress OpenEdge developers with deep knowledge of the ABL transaction model, ProDB schema management, OpenEdge Management, and legacy Progress 4GL application modernization toward ABL OO class-based architecture typically bill at $150 to $275 per hour. Monthly retainer ranges: $1,800 to $3,200 per month for advisory engagements (15 to 22 hours per month); $2,500 to $5,800 per month for active maintenance. Progress OpenEdge retainer rates are moderate relative to other legacy enterprise platforms; the talent pool is small but not as constrained as RPG or Natural.
What should a Progress OpenEdge developer retainer agreement include?
A Progress OpenEdge developer retainer agreement should specify: OpenEdge version (11.x vs 12.x; significant differences in PAS architecture, ABL OO class support, and ASSIGN statement behavior); ProDB schema change scope (whether the retainer covers reviewing all programs that access a table when a new field or index is added — ABL programs that use dynamic queries or FOR EACH table: without specifying fields may be affected by schema changes); AppServer and PAS connection model (stateful vs stateless affects which ProDB records remain in scope across procedure calls and which must be explicitly re-fetched; the retainer should specify whether connection-model audits are in scope); transaction log review (whether the retainer covers reviewing the OpenEdge after-image log for partial transaction patterns after production incidents); and migration scope (whether the retainer includes converting legacy Progress 4GL to ABL OO class-based architecture, or migrating from ProDB to an external database via DataServer — both are significant separate engagement categories).
How should Progress OpenEdge developer retainer hours be logged?
Log each Progress OpenEdge retainer session with the relevant specifics. For transaction scope bugs: program name (INVBATCH.p), ProDB file name (wh_inventory), loop type (FOR EACH), DML statement (ASSIGN inv_qty = inv_qty + received_qty), implicit transaction behavior (each ASSIGN committed independently), interruption scenario (network failure at Location 9), inconsistent-record count (3 → 0), fix (DO TRANSACTION: ... END. wrapping inner location loop plus ON ERROR UNDO, LEAVE), test scenario (simulated network interruption test), hours (3.5h). For FIND without AVAILABLE: program name, table name (customer), FIND condition (cust_id = input_id), missing AVAILABLE check, runtime error on unmatched record, unapplied count (4 → 0), fix (IF AVAILABLE guard plus error log), hours (2h). For SHARE-LOCK upgrade failure: program name, table, lock type requested vs needed, concurrent session scenario, retry count (8 → 0), fix (EXCLUSIVE-LOCK from loop start), hours (2.5h).