• The Four Hundred
  • Subscribe
  • Media Kit
  • Contributors
  • About Us
  • Contact
Menu
  • The Four Hundred
  • Subscribe
  • Media Kit
  • Contributors
  • About Us
  • Contact
  • 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

    Share this:

    • Share on Reddit (Opens in new window) Reddit
    • Share on Facebook (Opens in new window) Facebook
    • Share on LinkedIn (Opens in new window) LinkedIn
    • Share on X (Opens in new window) X
    • Email a link to a friend (Opens in new window) Email

    Tags: Tags: 400guru, API, FHG, Four Hundred Guru, IBM i, RPG, SQL

    Sponsored by
    WorksRight Software

    Do you need area code information?
    Do you need ZIP Code information?
    Do you need ZIP+4 information?
    Do you need city name information?
    Do you need county information?
    Do you need a nearest dealer locator system?

    We can HELP! We have affordable AS/400 software and data to do all of the above. Whether you need a simple city name retrieval system or a sophisticated CASS postal coding system, we have it for you!

    The ZIP/CITY system is based on 5-digit ZIP Codes. You can retrieve city names, state names, county names, area codes, time zones, latitude, longitude, and more just by knowing the ZIP Code. We supply information on all the latest area code changes. A nearest dealer locator function is also included. ZIP/CITY includes software, data, monthly updates, and unlimited support. The cost is $495 per year.

    PER/ZIP4 is a sophisticated CASS certified postal coding system for assigning ZIP Codes, ZIP+4, carrier route, and delivery point codes. PER/ZIP4 also provides county names and FIPS codes. PER/ZIP4 can be used interactively, in batch, and with callable programs. PER/ZIP4 includes software, data, monthly updates, and unlimited support. The cost is $3,900 for the first year, and $1,950 for renewal.

    Just call us and we’ll arrange for 30 days FREE use of either ZIP/CITY or PER/ZIP4.

    WorksRight Software, Inc.
    Phone: 601-856-8337
    Fax: 601-856-9432
    Email: software@worksright.com
    Website: www.worksright.com

    Share this:

    • Share on Reddit (Opens in new window) Reddit
    • Share on Facebook (Opens in new window) Facebook
    • Share on LinkedIn (Opens in new window) LinkedIn
    • Share on X (Opens in new window) X
    • Email a link to a friend (Opens in new window) Email

    A New IBM i Entity Takes Shape: Power By Fortra Big Blue Is Right On Schedule With Fall 2026 IBM i Tech Refreshes

    Leave a ReplyCancel reply

TFH Volume: 36 Issue: 38

This Issue Sponsored By

  • Maxava
  • New Generation Software
  • WorksRight Software
  • Computer Keyes
  • Manta Technologies

Table of Contents

  • Power Systems Turns In Another Great Quarter
  • Big Blue Is Right On Schedule With Fall 2026 IBM i Tech Refreshes
  • Guru: Environment Variables – Global Variables Wearing Hiking Boots
  • A New IBM i Entity Takes Shape: Power By Fortra
  • IBM i PTF Guide, Volume 28, Number 38

Content archive

  • The Four Hundred
  • Four Hundred Stuff
  • Four Hundred Guru

Recent Posts

  • Power Systems Turns In Another Great Quarter
  • Big Blue Is Right On Schedule With Fall 2026 IBM i Tech Refreshes
  • Guru: Environment Variables – Global Variables Wearing Hiking Boots
  • A New IBM i Entity Takes Shape: Power By Fortra
  • IBM i PTF Guide, Volume 28, Number 38
  • Critical PHP2Shell Security Vulns Evoke ‘Black October’
  • Why Fortra Partnered with FalconStor to Launch Continuity Care: A Validated Recovery Service for IBM i
  • Guru: Analyzing User Session Statistics, Part 2
  • Fresche Launches OpsIQ, a Free AI-Based Operation Assistant for IBM i
  • IBM i PTF Guide, Volume 28, Number 37

Subscribe

To get news from IT Jungle sent to your inbox every week, subscribe to our newsletter.

Pages

  • About Us
  • Contact
  • Contributors
  • Four Hundred Monitor
  • IBM i PTF Guide
  • Media Kit
  • Subscribe

Search

Copyright © 2025 IT Jungle