DEV Community

Daniel Jonathan
Daniel Jonathan

Posted on AI-assisted

The Green Screen That Wouldn't Let Go

How we taught a robot to rescue 15 plus years of accounting data from a 1990s computer — and then taught other robots to babysit it

Picture an accounting system older than the web. No mouse. No windows. Just a black screen with green text where you type short codes and press Enter, all day.

A client was finally leaving that system. But first they needed its history out: reports for over a thousand companies, each spanning multiple years. For every company and year, someone had to produce a yearly journal report, twelve monthly VAT reports, and a summary. That added up to tens of thousands of exports — with no export button, no API, and no bulk extraction tool.

So we built a robot that types like a human. This is the story of making it run unattended for days — told in seven pictures. Every picture exists because something broke.

The map

Here is the complete self-healing architecture:

At the bottom: the AS/400 accounting system. Above it: two macro robots running in parallel (one for yearly reports, one for monthly VAT reports). Above them: a supervisor that watches their activity, restarts failed sessions, and dismisses frozen error popups. Off to the side: alert emails fired as events happen, and a daily audit report.

The entire stack is built from exactly two kinds of parts: terminal macros (EXTRA! Basic — the robots that do the typing) and PowerShell scripts (everything that watches, restarts, reconciles, and reports). No RPA platform, no framework, no license fees — two scripting languages, one of them frozen in 1995, and a lot of respect for failure modes.

Full disclosure: an AI coding assistant pair-programmed much of it with me — writing and revising the macros and PowerShell, and helping diagnose the failure signatures you're about to read from their log traces. There's something fitting about a modern AI patiently writing 1995-era Basic for a 1990s green screen.

Every layer follows one fundamental rule:

A robot cannot be trusted to report its own death.
Every layer exists to catch a failure the layer below it cannot see.

Act 1: You can't automate what you can't see

Our first prototype simulated keypresses aimed at the terminal window. It worked in a demo, but failed in production: it was typing blind, unable to read screen state or error popups.

Switching to Attachmate EXTRA!'s macro engine (EXTRA! Basic) gave us direct COM access to the terminal's 80x24 screen memory buffer. The robot got eyes.

Lesson 1 — Being able to see matters more than having nice tools.

Act 2: Reading a screen like it's 1979

Reading an 80x24 screen means processing 1,920 raw characters with no DOM, links, or page load events. The robot must deduce its screen state before acting.

This state check carries a critical navigation trap, because two different screens sit one letter apart:

  • Identificatie — the login screen: the module selector you land on right after logging in. Typing the module code opens the company list.
  • Indentificatie — the post-login screen: the company list header, inside the module. Note the extra n: nearly the same word, but it marks a completely different state.

If the robot mistakes the company list for the login screen and re-sends the module code, those keystrokes land straight in the company search field, corrupting the search and derailing the loop.

So the robot checks for the post-login Indentificatie first, and only then for the login screen's Identificatie — that one-letter label is each screen's unique fingerprint, and the check order is what keeps navigation commands out of data input fields.

The whole thing runs as one loop: navigate to the company list, type the company name, reach its main menu, export that year's reports, write the result in the ledger — then repeat for the next company and year. When a company turns out not to exist in the system (or hides behind a password popup), the robot doesn't die: it dismisses the popup, logs the skip, returns to the company list, and carries on with the next one.

Even a screen the robot cannot recognize isn't an immediate stop. It saves a snapshot, logs the error, and tries to find its way back to the company list to carry on with the next company. Only when that recovery also fails does the log stop moving — and a log with no fresh entries is exactly what the supervisor treats as a hang signal (Acts 4 and 6). The loop never ends with a shrug: it either continues, skips, or hands over to the watchdog.

Lesson 2 — State recognition is everything. Never send navigation commands into a data input field.

Act 3: The crash that hid inside its own evidence

The AS/400 host needs time to draw screens. We initially waited 1.5 seconds after keystrokes, then cut it to 500ms to speed up tens of thousands of runs.

That triggered a race condition: popups render onto the terminal screen after network traffic goes quiet. At 500ms, the robot checked early, saw no message, and panicked. Worse, its diagnostic screen dump — the full 1,920 characters written to an error file — captured the screen a few milliseconds later, after the popup arrived, hiding the crash inside its own evidence.

We settled on a 1-second host settle time (g_HostSettleTime), backed by multi-pass retries on late-drawing screens.

Lesson 3 — With old systems, patience isn't a weakness. It is the protocol.

Act 4: The error window the robot cannot see

When the macro engine encounters a fatal runtime exception, it displays a modal popup — instantly freezing the script. The robot cannot detect its own crash or write a log entry. You cannot ask an unconscious patient to take their own pulse.

The watchdog lives inside the supervisor and is almost embarrassingly small: on every poll, a Win32 call looks for the dialog by its exact window title, and if found, posts an Enter keystroke straight to that window handle — no focus stealing, nothing typed into whatever window happens to be active:

# every supervisor poll — exact title only, never a pattern
$hwnd = [Win32]::FindWindow($null, "Extra! Basic Error")
if ($hwnd -ne [IntPtr]::Zero) {
    [Win32]::PostMessage($hwnd, $WM_KEYDOWN, $VK_RETURN, [IntPtr]::Zero)
    [Win32]::PostMessage($hwnd, $WM_KEYUP,   $VK_RETURN, [IntPtr]::Zero)
}
Enter fullscreen mode Exit fullscreen mode

Two rules make it safe:

  • Exact window title matching. FindWindow with a broad or generic title could post Enter into an unrelated application. One confirmed title, nothing else.
  • Audit-logged alerts. Every intervention is written to the supervisor's own dated log and triggers an email alert attempt — failed deliveries are themselves logged, never swallowed.

Dismissing the error window only clears the process. The real health check is: did the robot's log resume updating afterward?

Lesson 4 — Anything that can't watch itself needs an outside watcher. Clearing a modal doesn't mean the process survived.

Act 5: The day the computer lied about the time

The supervisor treats a log with no new entry for five minutes as a hang signal.

Initial versions checked the file's LastWriteTime, producing false alarms: with a log held open for appending for hours, that metadata goes stale — entries land on disk while the recorded timestamp lags behind. Worse, the watcher's own read of the file incidentally forced the metadata to refresh, so every false "stalled" alert was followed by a false "recovered" exactly one poll later. The watcher was curing the stall it had just reported.

The fix: make the log's content the primary signal. Every log entry starts with a timestamp the robot wrote itself, so the supervisor reads the tail and trusts the newest one it can parse:

$tail = Get-Content -Path $Path -Tail 5
for ($i = $tail.Count - 1; $i -ge 0; $i--) {
    if ($tail[$i] -match "^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})") {
        return [datetime]::ParseExact($Matches[1], "yyyy-MM-dd HH:mm:ss", $null)
    }
}
return $Fallback   # LastWriteTime — only for a brand-new log with no parseable line yet
Enter fullscreen mode Exit fullscreen mode

Lesson 5 — Judge whether something is alive by what it says, not by the label on its folder.

Act 6: Nobody wants to restart a robot at 3 a.m.

To eliminate manual midnight interventions, we created a supervisor script — a single PowerShell process running 24/7 — enforcing a tiered escalation ladder:

  1. Targeted Restart — after 5 minutes of log silence: kill only the frozen session, give the host a ~25-second cooldown, reopen, log back in, relaunch the macro. The parallel session keeps exporting throughout.
  2. Full Environment Reset — after 3 consecutive failed targeted restarts of the same session: both sessions come down and back up.
  3. Human Escalation — after 3 failed full resets: the supervisor stops retrying and fires an emergency alert, because at that point something is wrong that a relaunch won't fix.

The supervisor manages queue advancement, process lifecycles, and clean shutdown upon completion.

Lesson 6 — Don't just automate the work. Automate the person who babysits the work.

Act 7: A report doesn't exist until it's checked, filed, and written down

Generated reports undergo automated pipeline verification before queue status updates:

  • File Growth Stability: Polls each exported report — XLS and PDF alike — comparing file size at one-second intervals until it stops changing, before moving it.
  • Monotonic Queue Advancement: Updates queue files (companies_yearly.txt / companies_btw.txt) so crashes resume at the exact pending step.
  • Daily Reconciliation Audit: A 07:00 AM scheduled task reconciles disk files against logged status, reporting missing deliverables or unrecorded files.

Lesson 7 — Compare beliefs with facts daily. Let an audit confess the difference.

What this old machine taught us

Seven lessons, but really three themes:

  • Observe everything. Pick the runtime that can see (Act 1), recognize state before acting (Act 2), and judge liveness by what a thing says, not what its metadata claims (Act 5).
  • Distrust everything. Timing (Act 3), your own crash evidence (Act 3), a dismissed error dialog (Act 4), and even your own ledger (Act 7) — each lied to us at least once.
  • Automate the response, not just the work. An outside watcher for what can't watch itself (Act 4), an escalation ladder instead of a pager (Act 6), and bookmarks that make any crash cost one report, not one night (Act 7).

None of these lessons are really about old computers. An unreliable partner, no shared clock, failures that make no sound, progress that must survive a crash — that's the everyday physics of any two systems working together. Modern platforms just hide it well enough that you can pretend otherwise.

The old green screen refuses to let you pretend.


Top comments (0)