Blog › ICP guides

Visual FoxPro developer on retainer: PRIVATE variable scope, VFP 9, Microsoft FoxPro, and xBase on monthly retainer

October 8, 2026 · ~15 min read

A Visual FoxPro developer was maintaining a legacy order management application in VFP 9 for a regional wholesaler. The application processed orders through a multi-step workflow: the main program called DO validate_order.prg to validate the order, DO calculate_totals.prg to compute totals, and DO update_status.prg to finalize the order status. The main program then read m.order_status to determine whether to route the order to shipping or to hold it for review. On three consecutive batch runs, 3 orders were routed incorrectly — sent to shipping when they should have been held — because m.order_status read as empty (.F. or blank) after DO update_status.prg returned. The orders existed and update_status.prg had set m.order_status correctly inside the subprogram — but the value was not visible to the main program after the subprogram returned.

The root cause was how VFP 9’s PRIVATE variable scope works with DO-style subprogram calls. Inside update_status.prg, the developer had written: PRIVATE m.order_status followed by m.order_status = "HOLD" or m.order_status = "SHIP" based on validation rules. A PRIVATE variable in Visual FoxPro is local to the procedure where it is declared. When update_status.prg returned via RETURN, VFP destroyed the PRIVATE m.order_status variable — it existed only for the lifetime of the update_status.prg execution. The main program’s subsequent attempt to read m.order_status found no variable with that name (or found a stale outer-scope variable if one existed), resulting in empty or wrong status. Three orders with HOLD status were never signaled to the main program; the main program defaulted to shipping them.

The root of the problem is what PRIVATE means in Visual FoxPro’s memory variable scope model. VFP has three scope declarations: PRIVATE (the default if no declaration is made) declares a variable as visible to the current procedure and all procedures it calls (DO calls descend into the variable’s scope), but the variable is destroyed when the declaring procedure returns; PUBLIC declares a variable as global — it persists for the entire VFP session until explicitly released with RELEASE or the session ends; LOCAL declares a variable as strictly local to the declaring procedure and invisible even to procedures it calls. The m. prefix convention identifies a memory variable (as distinct from a field name with the same spelling), but the prefix does not affect scope — m.order_status is a memory variable with the same scope rules as any other variable. When update_status.prg declared PRIVATE m.order_status and set it to “HOLD”, that value was valid inside update_status.prg. The moment RETURN executed, PRIVATE m.order_status was released. The main program reading m.order_status after DO update_status found nothing.

The fix was to change the PRIVATE declaration to PUBLIC in update_status.prg: PUBLIC m.order_status. With PUBLIC scope, m.order_status persisted after DO update_status.prg returned and was visible to the main program. An alternative fix — preferable for reusable subprograms — was to replace the DO subprogram call style with a function call style: FUNCTION update_status() ... RETURN m.order_status, called as m.order_status = update_status() in the main program; in this form, the returned value is the function’s return value, not a side-effect variable, and the scope issue disappears entirely. Wrong order status propagations: 3 → 0 after changing to PUBLIC. The investigation — understanding VFP’s PRIVATE vs PUBLIC vs LOCAL scope model, tracing the DO update_status.prg call chain, and confirming that m.order_status was destroyed on RETURN — took 2 hours.

Visual FoxPro, the xBase model, and VFP 9’s variable scope

FoxBase was developed by Fox Software in 1984 as a high-performance dBase-compatible database system. FoxPro 1.0 followed in 1989, and FoxPro 2.0 in 1991 introduced Rushmore query optimization — a technology for fast tag-based filtering that uses CDX index tags to resolve FOR conditions without full table scans when the condition fields are indexed. Microsoft acquired Fox Software in 1992 and released Visual FoxPro 3.0 in 1995 with full object-oriented programming support. VFP 9.0 arrived in 2004 as the last Microsoft release of the product; Microsoft discontinued Visual FoxPro with end-of-life in January 2015. The xBase family also includes Clipper (a FoxBase/dBase-compatible compiler popular in the late 1980s and 1990s), dBASE Plus (dBase LLC’s current separate product lineage), and Harbour (an open-source xBase compiler actively maintained today, compatible with much VFP and Clipper syntax). VFP 9 remains in active production use at thousands of organizations running legacy order management, distribution, and government applications built in the 1990s and 2000s; the combination of end-of-life status and a still-large installed base makes VFP developer retainers one of the most consistently in-demand legacy software maintenance engagements.

VFP’s memory variable scope model is central to understanding both its legacy application architecture and its most common maintenance bugs. PRIVATE is the default scope for any variable that is simply assigned without an explicit declaration (m.order_status = "HOLD" with no prior declaration creates a PRIVATE variable automatically). A PRIVATE variable is visible to the procedure that creates it and to all procedures it calls via DO — child procedures can read and modify the PRIVATE variable. However, when the procedure that declared the PRIVATE variable returns, that variable is destroyed. PRIVATE variables can also shadow outer-scope variables of the same name: if the main program has a PRIVATE m.counter in scope when it calls a subprogram, and the subprogram declares its own PRIVATE m.counter, the subprogram’s declaration creates a new variable that hides the main program’s variable for the duration of the subprogram call; when the subprogram returns, the subprogram’s PRIVATE m.counter is destroyed and the main program’s original m.counter is visible again. PUBLIC variables persist for the entire VFP session and must be released explicitly with RELEASE varname or RELEASE ALL; the danger of PUBLIC scope is polluting the global namespace with variables from subprograms that accumulate across repeated calls. LOCAL variables are strictly invisible even to called procedures — a child DO call cannot read or modify a LOCAL variable from the calling procedure, making LOCAL the correct choice for loop counters and scratch variables that must not leak to child calls. The m. prefix convention distinguishes a memory variable from a field name with the same spelling: in a work area where the ORDERS table is open, order_id refers to the field, while m.order_id unambiguously refers to the memory variable. The prefix is a convention, not a scope modifier — it does not change whether the variable is PRIVATE, PUBLIC, or LOCAL.

VFP’s xBase data operations follow the work area model inherited from dBase. USE tablename opens a DBF table in the current work area; SELECT n or SELECT alias switches between work areas (up to 32,767 simultaneous open tables in VFP 9). SET RELATION TO fieldname INTO alias establishes an implicit record positioning relationship between two open tables, so that moving the record pointer in the parent table automatically positions the child table’s record pointer. SEEK value performs an index-based lookup and requires SET ORDER TO tagname to select which CDX index tag to use; CDX is VFP’s multi-tag compound index format (a single .CDX file contains multiple named index tags, replacing the single-tag .NTX format used by Clipper). LOCATE FOR condition performs a sequential scan when no index is available. SCAN ... ENDSCAN iterates all records meeting an optional FOR condition; Rushmore optimization applies to SCAN only when the FOR condition field has a CDX tag, otherwise the SCAN performs a full sequential table read. DML operations include REPLACE fieldname WITH value for field updates, APPEND BLANK to add a new empty record, DELETE to mark a record with the deletion flag, RECALL to unmark it, PACK to physically remove all deleted records (destructive and irreversible without a backup), and ZAP to remove all records from the table. VFP also supports SQL-style operations: UPDATE-SQL for set-based updates, SELECT-SQL for queries that return cursors into memory, and COPY TO filename TYPE XLS for Excel export.

Visual FoxPro 3.0 introduced full object-oriented programming that coexists with the procedural DO-style subprogram architecture in the same application. DEFINE CLASS classname AS parentclass ... ENDDEFINE defines a class; CREATEOBJECT("classname") instantiates it; THIS is the self-reference inside methods, analogous to self in Python or this in Java. The VFP Form Designer produces .SCX form files (binary container files that store form and control definitions); the Report Designer produces .FRX report files. At the database layer, VFP’s DBC database container is a .DBC file accompanied by .DCT (data dictionary) and .DCX (index) files; the DBC stores referential integrity triggers, stored procedures, persistent relations between tables, and field-level validation rules. SET DATABASE TO dbcname and OPEN DATABASE activate the DBC for a session. VFP 9 applications can also be compiled as COM DLLs called from VB6 or .NET host applications via CreateObject — the VFP COM server model allows legacy VFP business logic to be invoked from newer host applications without full migration. Rushmore query optimization, VFP’s most distinctive performance technology, uses CDX index tags to resolve FOR conditions (in SCAN, LOCATE, COPY FOR, DELETE FOR, and other commands) without reading every record sequentially; for Rushmore to apply, the fields in the FOR condition must have CDX tags, the condition must be expressible in a form Rushmore recognizes, and SET OPTIMIZE must be ON (the default). VFP 9 is the last version; no VFP 10 was released.

Visual FoxPro developer retainer rates span a wide range by experience level. Entry-level VFP developers typically bill at $60–$110 per hour. Mid-level VFP programmers with DBC management, Rushmore optimization, OOP class architecture, and SCX/FRX experience typically bill at $90–$160 per hour. Senior Visual FoxPro developers with VFP COM server expertise, deep scope model knowledge, and VFP-to-modern-stack migration experience typically bill at $130–$240 per hour. Monthly retainer engagements range from $1,500–$2,800/mo for advisory (12–20 hours) to $2,000–$4,800/mo for active maintenance.

Typical Visual FoxPro retainer work and what it looks like in a work log

PRIVATE/PUBLIC variable scope bugs from DO-style subprogram calls are the most invisible category of VFP retainer work. The opening scenario is representative: a main program calls a subprogram via DO subprogram.prg to produce a result that the main program reads as a side-effect variable after the DO call returns. The subprogram declares the variable as PRIVATE, sets it correctly, and returns. The PRIVATE declaration causes VFP to destroy the variable on RETURN, leaving the main program with an empty or stale value. In development, the developer tests update_status.prg independently, reads m.order_status inside the subprogram before RETURN, confirms the value is set, and marks the test passed — the test never exercises the main program’s read of m.order_status from outside the subprogram after RETURN. The fix is either changing PRIVATE to PUBLIC (so the variable persists beyond RETURN) or switching to function-return style (m.order_status = update_status()) where the return value is explicit and scope destruction is irrelevant. Work log entry: “update_status.prg: PRIVATE m.order_status set to 'HOLD' or 'SHIP' inside subprogram; DO-style call from main program; variable destroyed on RETURN; main program read empty value and defaulted to shipping; wrong order routings: 3 → 0 via PUBLIC m.order_status; 2h.”

DO vs function call return value bugs are the second common VFP retainer pattern. In VFP, DO procname calls a procedure; the procedure can set PRIVATE variables that the caller reads, but this side-effect pattern is fragile because of PRIVATE scope destruction. The preferred alternative is calling the procedure as a function and consuming its return value: m.result = procname() or m.result = procname(arg1, arg2). A related bug involves PARAMETERS count mismatches: in the DO-style call model, DO procname WITH arg1, arg2, arg3 passes positional parameters, and the called program uses a PARAMETERS statement to receive them. If the caller passes fewer arguments than the PARAMETERS statement declares, VFP initializes the missing parameters to .F. (logical false) silently — no error, no warning, and the subprogram executes with a wrong default value for the missing argument. A subprogram that expects a numeric quantity parameter and receives .F. because the caller omitted the third argument will compute a quantity of 0 without complaint. Switching to LPARAMETERS (local parameter declaration) and adding parameter count validation (IF PARAMETERS() < 3 ... ENDIF) catches these mismatches. Work log entry: “calc_discount.prg: PARAMETERS m.cust_id, m.order_total, m.discount_tier; caller passed 2 args; m.discount_tier received as .F.; discount calculated as 0 for all tier-3 customers; wrong discounts: 7 orders → 0 after adding IF PARAMETERS() < 3 guard and fixing caller to pass tier argument; 1.5h.”

SCAN…ENDSCAN lock and performance bugs are the third common VFP retainer pattern. A SCAN loop on a large DBF file iterates every record sequentially unless Rushmore optimization can apply. Rushmore applies only when the FOR condition field in the SCAN has a CDX index tag and the condition is in a form Rushmore recognizes (simple equality, range, or tag-intersectable expression). A SCAN loop with no FOR condition, or with a FOR condition that references a function or a non-indexed field, performs a full sequential read of every record in the DBF — on a 500,000-record DBF this can take tens of seconds or minutes. In a multi-user environment where multiple workstations share the same DBF over a network, a full-table SCAN also holds shared read locks on pages as it progresses, which can block concurrent UPDATE or DELETE operations on those pages. The fix is to add a CDX tag on the filter field: INDEX ON filter_field TAG filter_field_tag, then confirm that VFP’s Rushmore optimization is engaged by setting SET TALK ON and observing the optimization level in the status bar during development, or by measuring scan time before and after. Work log entry: “process_backorders.prg: SCAN FOR m.status_code = 'BACK' on orders.dbf (487,000 records); no CDX tag on status_code; Rushmore inactive; full table scan; 42 seconds per run; concurrent UPDATE blocked during scan; built INDEX ON status_code TAG status_code; scan time: 42s → 1.2s; UPDATE blocking: resolved; 2h.”

Track Visual FoxPro developer retainer hours without the status emails

When a 2-hour investigation traces 3 wrong order routings to a PRIVATE variable destroyed when update_status.prg returned — m.order_status set to ‘HOLD’ inside the subprogram, invisible to the main program after RETURN — the work log must name the subprogram, the PRIVATE declaration, the variable, and the routing count before and after. HourTab gives your VFP retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log naming the scope declaration and the fix. No client login. No status emails. CSV in, URL out.

See HourTab pricing →

How HourTab tracks Visual FoxPro retainer hours

PRIVATE scope bugs are invisible by the same mechanism that makes them dangerous: in sequential single-user testing, the developer tests update_status.prg independently. The m.order_status value is read inside the subprogram where it is valid; the developer verifies that the assignment worked and that the value is correct. The test does not call back from the main program’s scope after RETURN — the test script or interactive debug session ends after confirming the subprogram set the variable, without then checking whether the main program can read it after the DO call returns. There is no VFP runtime error, no warning, no indication in any debug output or log that m.order_status was destroyed — PRIVATE scope destruction is silent by design, the normal cleanup behavior of a returning procedure. The developer shipping the subprogram had good reason to believe it was correct; every isolated test confirmed it. The compound failure — PRIVATE scope destruction silently removing the side-effect variable that the main program expected to read — only surfaced in production when the full call chain ran and the main program’s branching logic read the destroyed variable.

The work log needs to name the mechanism: which subprogram, the PRIVATE declaration, the variable name, the DO call style, how the scope was fixed (PUBLIC or function RETURN), and the wrong-routing count before and after (3 → 0). A log entry that says “fixed order routing bug, 2h” is not auditable. A log entry that names update_status.prg, the PRIVATE m.order_status declaration, the DO call style, the PRIVATE-to-PUBLIC fix, and the 3 mis-routed orders that were corrected is auditable and defensible. HourTab gives Visual FoxPro developers a public retainer-hours URL they send to clients — regional wholesalers, distributors, and government agencies running order management, inventory, and billing applications built in Visual FoxPro 6 through 9 in the late 1990s and 2000s, maintained today on VFP 9 on Windows Server with DBF files and DBC database containers. Comparative context: VFP PRIVATE scope destruction has structural overlap with adjacent bugs on other xBase platforms. Clipper retainers cover the same xBase family, where SEEK index bugs on NTX single-tag indexes cause silent wrong-record lookups when the active index is not the expected tag — both are silent failures with no runtime error where the developer’s test environment does not reproduce the production failure condition. dBASE retainers cover the deletion flag and SET DELETED ON/OFF model, where records marked for deletion with the deletion flag remain readable at SET DELETED OFF (the default) and produce wrong counts and wrong results in applications that assume deleted records are invisible — both involve a platform-specific visibility behavior that is correct by specification but surprises developers expecting more conventional database semantics.

FAQ: Visual FoxPro developer retainers

What does a Visual FoxPro developer on retainer typically do?

A Visual FoxPro developer on monthly retainer covers variable scope audit (reviewing all PRIVATE, PUBLIC, and LOCAL declarations in DO-style subprogram call chains to confirm that PRIVATE variables set as side effects in subprograms are not expected by callers after RETURN; confirming that PUBLIC scope or function-return style is used where the calling program needs to consume a value produced by a subprogram); CDX index management and tag selection for SEEK and Rushmore query optimization (auditing all SEEK operations to confirm SET ORDER TO tagname precedes each SEEK; confirming that CDX compound index tags exist on all fields used in FOR conditions to enable Rushmore optimization and avoid full table scans on large DBF files); DBC referential integrity and trigger debugging (reviewing database container stored procedures and referential integrity triggers for partial-update states); VFP COM server maintenance (auditing VFP applications compiled as COM DLLs called from VB6 or .NET for CreateObject instantiation errors, threading model mismatches, and memory leak patterns from unreleased object references); and VFP 9 to modern stack migration assessment (evaluating DBF table schemas, DBC containers, SCX form files, FRX report files, and application program structure for migration candidates to SQL Server, .NET, or Harbour).

What Visual FoxPro scope work is most commonly underlogged?

PRIVATE variable scope bugs from DO-style subprogram calls are the most systematically underlogged Visual FoxPro retainer work. The pattern: a main program calls DO subprogram.prg to perform a calculation or status update; the subprogram declares PRIVATE m.result_variable and sets it to the computed value; the subprogram returns via RETURN; the main program reads m.result_variable and finds it empty, blank, or .F. because the PRIVATE declaration was destroyed when the subprogram returned. The subprogram set the variable correctly — it is valid and readable inside the subprogram for its entire execution. The value disappears the moment RETURN executes. No VFP runtime error, no warning, no indication in any log that the variable was destroyed. The developer testing the subprogram in isolation verifies the assignment worked; the test does not call back from the main program’s scope after RETURN, so the silent destruction of the PRIVATE variable is never observed in testing.

What are typical Visual FoxPro developer retainer rates?

Entry-level Visual FoxPro developers with experience in basic VFP 9 programming, DBF table operations, CDX index management, and standard DO-style subprogram architecture typically bill at $60 to $110 per hour. Mid-level VFP programmers with DBC database container management, referential integrity triggers, VFP OOP class definitions, SCX form design, FRX report design, and Rushmore query optimization experience typically bill at $90 to $160 per hour. Senior Visual FoxPro developers with deep VFP 9 internals knowledge, VFP COM server architecture, VFP-to-SQL-Server or .NET migration experience, and Harbour open-source xBase compatibility work typically bill at $130 to $240 per hour. Monthly retainer ranges: $1,500 to $2,800 per month for advisory engagements (12 to 20 hours per month); $2,000 to $4,800 per month for active maintenance. VFP retainer rates reflect the end-of-life status of the platform (January 2015) and the severely constrained talent pool of developers still actively working with VFP 9 codebases.

What should a Visual FoxPro developer retainer agreement include?

A Visual FoxPro developer retainer agreement should specify: VFP version (6, 7, 8, or 9; VFP 9 is the last Microsoft release and the most common for active maintenance; VFP 6 and 7 applications have differences in OOP support, DBC behavior, and SQL-SELECT cursor handling); application architecture style (DO-style procedural subprogram architecture versus OOP class-based architecture using DEFINE CLASS; mixed architectures are common in long-lived VFP codebases that evolved from FoxPro 2.x procedural style to VFP 3+ OOP); database container scope (whether the application uses a DBC database container with stored procedures, persistent relations, and referential integrity triggers, or standalone DBF files without a DBC); COM server scope (whether the VFP application is compiled as a COM DLL called from a VB6 or .NET host, requiring awareness of COM instantiation, threading model, and the VFP runtime installation on the server); and migration scope (whether the retainer includes assessment or active work on migrating VFP 9 application logic, DBF schemas, and DBC definitions to SQL Server, .NET, or the Harbour open-source xBase compiler).

How should Visual FoxPro developer retainer hours be logged?

Log each Visual FoxPro retainer session with the relevant scope declaration and call-chain specifics. For PRIVATE variable scope bugs: subprogram name (update_status.prg), variable name (m.order_status), scope declaration (PRIVATE m.order_status inside update_status.prg), DO call style (main program calls DO update_status.prg; reads m.order_status after RETURN), incorrect scope behavior (PRIVATE variable destroyed on RETURN; main program reads empty/wrong value), fix type (changed PRIVATE to PUBLIC in update_status.prg, or replaced DO call with function-return style: m.order_status = update_status()), wrong-routing count before and after (3 orders mis-routed to shipping instead of hold; 3 → 0 after fix), hours (2h). For CDX index and SEEK bugs: table name, SEEK field, missing CDX tag, fix (built new CDX tag via INDEX ON field TAG tagname), wrong-lookup count before and after, hours. For SCAN loop performance: table name, DBF record count, missing indexed FOR condition, fix (added CDX tag on filter field), scan time before and after, hours.