Guru: Environment Variables – Global Variables Wearing Hiking Boots
November 2, 2026 Gregory Simmons
Sometimes a program knows one tiny piece of information that another program, several calls farther down the stack, needs to know.
The obvious solution is to add another parameter. But sometimes that requires that new parameter added to one or more programs in the call stack. Before long, we’re passing a mushroom basket through half the forest even though most of the programs along the way have no interest in what’s inside it.
There is another option that has been available on IBM i for a very long time: environment variables. IBM i supports both job-level and system-level environment variables. We’re going to stick with job-level variables for this article.
A job-level environment variable gives us a simple name and value that can be created by one program and retrieved by another program running much farther down the same job’s call stack. No additional parameter required.
For our example, we’re going to create an environment variable named MUSHROOM_HABITAT and assign it a value of FOREST. Environment variables are essentially name/value pairs.
Job-level environment variables are stored outside of the programs running within the job. They remain available for the duration of the job or until they are explicitly deleted. That makes them particularly interesting for our call-stack example.
IBM gives us several different ways to work with them, including APIs and CL commands. And, as we’ve come to expect on modern IBM i, SQL can make some of this considerably easier.
Rather than write one program that talks to itself, let’s write two, the way this would actually happen on a real call stack. We’ll call the program that establishes the habitat FORAGE, and the program that needs to know about it WARNINGS. FORAGE will call WARNINGS directly, with nothing shared between them except the job they’re both running in.
Let’s start by planting our first mushroom. Db2 for i provides the SYSTOOLS.PUTENV() scalar function to create or change an environment variable for the current job.
Here’s FORAGE:
**free
Ctl-Opt Main(Forage);
Dcl-Pr Warnings ExtPgm('WARNINGS');
End-Pr;
Dcl-Proc Forage;
Dcl-S returnCode Int(10);
Exec SQL
Values SYSTOOLS.PUTENV('MUSHROOM_HABITAT', 'FOREST')
Into :returnCode;
Warnings();
End-Proc;
If MUSHROOM_HABITAT doesn’t already exist, PUTENV() creates it. If it does exist, its value is changed.
Under the covers, IBM’s SYSTOOLS.PUTENV() function uses the putenv() API. The SQL function just gives us a friendlier interface, allowing us to provide the environment variable name and value as two separate parameters.
A return value of zero tells us the operation was successful.
We now have an environment variable named MUSHROOM_HABITAT with a value of FOREST, and FORAGE is about to call WARNINGS, a completely separate program, to see if it made the trip.
Creating the variable doesn’t do us much good unless another program can retrieve it. Here’s WARNINGS:
**free
Ctl-Opt Main(Warnings);
Dcl-Proc Warnings;
Dcl-S habitat Varchar(1024);
Exec SQL
Values SYSTOOLS.GETENV('MUSHROOM_HABITAT')
Into :habitat;
Dsply ('Habitat: ' + habitat);
End-Proc;
Call FORAGE, and WARNINGS displays:
Habitat: FOREST
GETENV() retrieves an environment variable from the current job and returns its value as a variable-length character string of up to 1,024 characters.
There is one important detail here. If the environment variable doesn’t exist, GETENV() returns the SQL null value. We’ll take advantage of that shortly.
If you want to look at more than one (JOB|SYSTEM) environment variable, IBM also provides the QSYS2.ENVIRONMENT_VARIABLE_INFO view:
SELECT * FROM QSYS2.ENVIRONMENT_VARIABLE_INFO WHERE ENVIRONMENT_VARIABLE_TYPE = 'JOB';
And outside of SQL, you can use the command WRKENVVAR LEVEL(*JOB|*SYS).
There are plenty of ways to peek into the environment and see what’s growing there.
It’s worth pausing on what just happened. FORAGE and WARNINGS are two separate programs, called as two separate steps on the call stack, and nothing was passed between them: no parameter, no return value, not even a shared file.
If habitat had been declared STATIC inside a procedure in FORAGE instead, none of this would have worked, and not because of anything to do with activation groups. STATIC storage belongs to the module that declares it. WARNINGS has no way to reach into FORAGE’s module and read its static storage, whether the two programs happen to run in the same activation group or in separate ones.
A job-level environment variable doesn’t have that problem, because it isn’t attached to a module or an activation group at all. It’s attached to the job. FORAGE and WARNINGS don’t need to share a module, an activation group, or even the same compile. They only need to be running in the same job. That’s what lets the value survive a trip across the call stack, whatever activation groups the two programs happen to be running in.
Changing an existing environment variable looks suspiciously like creating one. That’s because it’s exactly the same operation.
Suppose conditions change mid-job, and FORAGE needs to move our mushroom from the forest to a meadow. FORAGE doesn’t need any new code to do it: the same PUTENV() call handles both creating and changing a variable:
Exec SQL
Values SYSTOOLS.PUTENV('MUSHROOM_HABITAT', 'MEADOW')
Into :returnCode;
The next time anything farther down the call stack asks for MUSHROOM_HABITAT (including WARNINGS, unchanged from before), it sees the new value:
Habitat: MEADOW
WARNINGS never needed to know the habitat changed. It just asked the job for the current value, same as always.
So far SQL has given us a wonderfully simple way to create, retrieve and change an environment variable.
You might expect there to be a SYSTOOLS.DELENV() function waiting for us.
Well, there currently isn’t.
For that operation, we could reach for the CL command RMVENVVAR, a natural companion to WRKENVVAR from earlier. It takes one parameter, the name of the environment variable to remove. But since FORAGE and WARNINGS are already written in RPG, we’ll stay there too and drop down to the Qp0zDltEnv() API instead. Of course, since this is a job-level environment variable, it will naturally be cleaned up when the job ends. But FORAGE created MUSHROOM_HABITAT, so FORAGE should be the one to clean it up once WARNINGS is done with it.
Fortunately, it isn’t difficult to call from RPG:
Dcl-Pr Qp0zDltEnv Int(10) ExtProc('Qp0zDltEnv');
name Pointer Value Options(*String);
End-Pr;
Back in FORAGE, after WARNINGS returns, we can remove the environment variable with:
returnCode = Qp0zDltEnv('MUSHROOM_HABITAT');
A return value of zero indicates success, while -1 indicates that the API failed.
Now we can use GETENV() one more time to make sure our mushroom really is gone. Because GETENV() returns null when the requested environment variable isn’t found, we’ll use an SQL null indicator:
Dcl-S nullIndicator Int(5);
Exec SQL
Values SYSTOOLS.GETENV('MUSHROOM_HABITAT')
Into :habitat :nullIndicator;
If nullIndicator < 0;
Dsply ('MUSHROOM_HABITAT has been deleted');
EndIf;
And there we have the entire lifecycle of a job-level environment variable, shared across two programs on the same call stack: FORAGE created it, WARNINGS retrieved it without ever being told directly, FORAGE changed it, WARNINGS retrieved the new value just as easily, and finally FORAGE cleaned up after itself.
I used SQL for most of this example because it makes for a short demonstration, but SQL isn’t our only option.
IBM i provides APIs for these operations as well. Among them are getenv(), putenv() and Qp0zDltEnv().
Their RPG prototypes could look something like this:
Dcl-Pr getenv Pointer ExtProc('getenv');
name Pointer Value Options(*String);
End-Pr;
Dcl-Pr putenv Int(10) ExtProc('putenv');
string Pointer Value Options(*String);
End-Pr;
Dcl-Pr Qp0zDltEnv Int(10) ExtProc('Qp0zDltEnv');
name Pointer Value Options(*String);
End-Pr;
Unlike the SQL PUTENV() function, the putenv() API expects a single string, so calling putenv() to set our mushroom habitat would look like this:
returnCode = putenv('MUSHROOM_HABITAT=FOREST');
There are also extended IBM i APIs, including Qp0zGetEnv() and Qp0zPutEnv(), that provide additional capabilities such as working with the CCSID associated with an environment variable.
For our purposes, though, the SQL functions make this demonstration much easier to follow.
There is one prerequisite worth mentioning. IBM documents that the SYSTOOLS.GETENV() and SYSTOOLS.PUTENV() functions require Option 13, System Openness Includes, to be installed.
This is a quick demonstration, so I’m calling these interfaces directly.
That doesn’t mean I’d recommend scattering calls to SYSTOOLS.GETENV(), SYSTOOLS.PUTENV() and Qp0zDltEnv() throughout a production application.
If environment variables became part of one of my applications, I’d export these procedures in a service program for all to use. Maybe something along the lines of:
environment_get() environment_set() environment_delete() environment_exists()
Then the programs using an environment variable don’t need to know whether the implementation uses an SQL function, an API, or something IBM gives us in the future.
After all, we’ve spent enough time talking about procedure-driven RPG that you didn’t think I was going to let you get away with scattering API calls throughout your applications, did you?
But knowing how to create an environment variable still doesn’t answer the more important question. Why would we want one?
You’ve already seen FORAGE and WARNINGS talk to each other directly. Now imagine a deeper call stack, with two more programs in between that have no interest in the habitat at all:
FORAGE
|
+-- IDENTIFY
|
+-- CLASSIFY
|
+-- WARNINGS
The FORAGE program knows that we’re currently looking for mushrooms in the FOREST.
Much farther down the call stack, WARNINGS needs to know the current habitat so it can determine which warnings should be applied.
IDENTIFY and CLASSIFY don’t (currently) care about the current habitat.
One way to get that information down to WARNINGS would be to add a parameter.
That means FORAGE passes the habitat to IDENTIFY.
IDENTIFY then passes it to CLASSIFY.
And CLASSIFY finally passes it to WARNINGS.
We’ve now changed the interfaces of three programs just to transport a value through two programs that have absolutely no use for it. We’re passing our mushroom basket through half the forest.
Instead, FORAGE could establish that MUSHROOM_HABITAT = FOREST
Then it makes its normal calls.
When execution eventually reaches WARNINGS, it can retrieve MUSHROOM_HABITAT from the job environment. We’ve passed some context down the call stack without coupling every program in between to that context.
And I think the word context is important here. If a program or procedure requires a value to perform its job, that value probably belongs in its interface.
If WARNINGS requires a mushroom identifier to do anything meaningful, hiding that identifier in an environment variable would make the code harder to understand, not easier.
Anyone looking at the procedure’s interface should be able to see the information it requires.
But sometimes the information we’re passing around isn’t really input to the operation. It’s context describing the environment in which that operation happens to be running.
Where did this request originate?
Is some optional application behavior enabled?
Which operating mode are we currently using?
What context was established much earlier in the call stack?
Those are the situations where an environment variable may be worth considering.
A simple rule of thumb might be that if the value is required to perform the operation, pass it as a parameter. If the value describes the context in which the operation is occurring, an environment variable might be a better fit.
It is important to note that job-level environment variables remain in the job environment until you delete it or the job ends. That means we need to be especially careful in long-running jobs.
Suppose one transaction establishes that MUSHROOM_HABITAT = FOREST and after that transaction completes, we forget to delete it. The next transaction running through that same job may now find MUSHROOM_HABITAT = FOREST, even though nobody established a habitat for that transaction.
That’s the sort of bug that can be particularly entertaining to find in production. So, if you’re using an environment variable to represent temporary context, establish clearly who owns it and who is responsible for removing it.
Environment variables also shouldn’t become a dumping ground for everything we don’t feel like passing as a parameter. Even a good pair of hiking boots can wander off the trail if nobody’s watching where they’re going, and a global variable used indiscriminately will do exactly that.
But used selectively, they give us another way to share a small amount of job context without unnecessarily changing every interface between the program that knows something and the program that needs to also know that something.
Sometimes the shortest path through the forest doesn’t require everyone to carry the mushroom basket.
Until next time, happy coding.
Gregory Simmons is a Project Manager with PC Richard & Son. He started on the IBM i platform in 1994, graduated with a degree in Computer Information Systems in 1997 and has been working on the OS/400 and IBM i platform ever since. He has been a registered instructor with the IBM Academic Initiative since 2007, an IBM Champion and holds a COMMON Application Developer certification. When he’s not trying to figure out how to speed up legacy programs, he enjoys speaking at technical conferences, running, backpacking, hunting, and fishing.
RELATED STORIES
Guru: When RNF0722 Won’t Let Your Legendary RPG Program Compile
Guru: Two More Small But Handy RPG Enhancements: LIKE(*EXT) And %LOOKUPNE
Guru: Beyond Three-Part Naming – Running SQL Across Remote IBM i Systems
Guru: Finding Data In The Forest – Exploring Three-Part Naming In SQL
Guru: SQL Sequences In RPG Let Db2 Handle The Counting
Guru: IBM i Job Log Detective Brings Structure To Job Log Analysis In VS Code
Guru: Managing The Lifecycle Of Your Service Programs – Updates Without Chaos
Guru: Are Binding Directories A Shortcut Or A Source Of Chaos?
Guru: Service Programs And Activation Groups – Design Decisions That Matter
Guru: Binder Source Is Your Service Program’s Owner’s Manual
Guru: Access Client Solutions 1.1.9.11 – Security First, With Continued Investment In SQL Tooling
Guru: Taming The CRTSRVPGM Command – Options That Can Save Your Sanity
Guru: CRTSRVPGM Parameters That Can Save or Sink You
Guru: A First Look at Bob, The IBM i Assistant That’s Closer Than You Think
Bob More Than Just A Code Assistant, IBM i Chief Architect Will Says
IBM Pulls The Curtain Back A Smidge On Project Bob
Big Blue Converges IBM i RPG And System Z COBOL Code Assistants Into “Project Bob”
Guru: When Attention Turns To You – Writing Your Own ATTN Program
Guru: WCA4i And Granite – Because You’ve Got Bigger Things To Build
Guru: When Procedure Driven RPG Really Works
Guru: Unlocking The Power Of %CONCAT And %CONCATARR In RPG
Guru: AI Pair Programming In RPG With Continue
Guru: AI Pair Programming In RPG With GitHub Copilot
Guru: RPG Receives Enumerator Operator
Guru: RPG Select Operation Gets Some Sweet Upgrades
Guru: Growing A More Productive Team With Procedure Driven RPG
Guru: With Procedure Driven RPG, Be Precise With Options(*Exact)
Guru: Testing URLs With HTTP_GET_VERBOSE
Guru: Fooling Around With SQL And RPG
Guru: Procedure Driven RPG And Adopting The Pillars Of Object-Oriented Programming
Guru: Getting Started With The Code 4 i Extension Within VS Code
Guru: Procedure Driven RPG Means Keeping Your Variables Local
Guru: Procedure Driven RPG With Linear-Main Programs
Guru: Speeding Up RPG By Reducing I/O Operations, Part 2
Guru: Speeding Up RPG By Reducing I/O Operations, Part 1
Guru: Watch Out For This Pitfall When Working With Integer Columns

