← All Articles

Scripting and Automation Cookbook

Script Editor Overview

The IMTerm Script Editor is built on CodeMirror - the same editor used in VS Code and many browser-based IDEs. Access it from any active session via the toolbar Script icon or Session > Script Editor.

Features: - JavaScript syntax highlighting and error underlining - Auto-complete for the session API - Template library (auto sign-on, data extraction, screen navigation) - Save scripts to your profile, share across sessions - Run button with live console output - One-click "Save as Scheduled" or "Save as Event Handler"

Scripts run in a sandboxed JavaScript environment with access only to the session API. There is no filesystem access, no network access beyond what IMTerm provides, and no access to other sessions. This is intentional for security.


Session API Reference

Every script receives a session object. All methods return Promises - use await.

Typing and Input

await session.type("WRKACTJOB")

Types the string into the current cursor position. Does not press Enter.

await session.sendKey("enter")
await session.sendKey("pf3")
await session.sendKey("pf12")
await session.sendKey("clear")
await session.sendKey("pa1")
await session.sendKey("tab")
await session.sendKey("backtab")
await session.sendKey("home")
await session.sendKey("newline")

Sends an AID key or cursor control key. PF keys are pf1 through pf24. Use enter (not Enter).

await session.setField("USERNAME", "JSMITH")

Sets the field with the given field ID directly, without moving the cursor or typing. Field IDs come from the screen definition. Use session.readScreen() to discover them.

Reading the Screen

const screen = await session.readScreen()

Returns the full screen as a 2D array of characters. screen[0] is row 1, screen[0][0] is column 1.

const text = await session.readText(row, col, length)

Reads length characters starting at row, col (1-indexed).

const field = await session.readField("USERNAME")

Reads the current value of the named field. Returns a trimmed string.

const fields = await session.readAllFields()

Returns an object mapping field IDs to their current values. Useful for extracting a complete record.

const cursor = await session.getCursor()
// Returns: { row: 5, col: 20 }
const oa = await session.getOIA()
// Returns: { connected: true, inhibited: false, insertMode: false, waitingForHost: false }

Reads the Operator Information Area. Use waitingForHost to know when the host is processing.

Waiting and Synchronization

await session.waitForUnlock(timeout_ms)

Waits until the keyboard is unlocked (host finished processing). This is the most important synchronization primitive. Always call it after sendKey("enter").

await session.waitForText(text, timeout_ms)

Waits until the given text appears anywhere on screen. Returns the position {row, col} where it was found, or throws if timeout expires.

await session.waitForScreen(pattern, timeout_ms)

pattern is either a string (text that must appear) or a regex. Waits up to timeout_ms milliseconds (default 10000).

await session.sleep(ms)

Pauses execution. Use sparingly - prefer waitForUnlock or waitForText instead of fixed sleeps.

Output and Utility

session.log("Found " + count + " records")

Writes to the script console. Visible in the editor's output panel while the script runs.

session.result(data)

Stores data as the script's output. Can be any JSON-serializable value. Visible in the console and returned by REST API calls that invoke the script.


Recipe 1: Auto Sign-On

The most common script - fills credentials and presses Enter without user interaction.

// Auto sign-on for IBM i
await session.waitForText("Sign On", 5000)

await session.setField("USER", "JSMITH")
await session.setField("PASSWORD", "secret123")
await session.sendKey("enter")
await session.waitForUnlock(10000)

// Verify we're on the main menu
const screen = await session.readScreen()
const title = screen[0].join("").trim()
session.log("Arrived at: " + title)

Usage: Save this as an event handler on on_connect to automatically sign in whenever the session connects.


Recipe 2: Navigate to a Specific Screen

Navigate the IBM i menu structure to a known screen.

// Go to Customer Maintenance
// From main menu: 1 → CRM menu → 2 → CUSTMNT screen

await session.waitForText("IBM i Main Menu", 5000)
await session.type("1")
await session.sendKey("enter")
await session.waitForUnlock()

await session.waitForText("CRM Menu", 5000)
await session.type("2")
await session.sendKey("enter")
await session.waitForUnlock()

await session.waitForText("Customer Maintenance", 5000)
session.log("On Customer Maintenance screen")

Recipe 3: Read Data from a Screen

Extract field values and return them as structured data.

// Extract customer data from CUSTMNT
await session.waitForText("Customer Maintenance", 5000)

const data = {
    custNo:   await session.readText(5, 20, 8),
    name:     await session.readText(7, 20, 40),
    address:  await session.readText(9, 20, 40),
    city:     await session.readText(11, 20, 20),
    phone:    await session.readText(13, 20, 15),
    balance:  await session.readText(15, 20, 12)
}

// Trim whitespace from all fields
for (const key of Object.keys(data)) {
    data[key] = data[key].trim()
}

session.log(JSON.stringify(data, null, 2))
session.result(data)

Recipe 4: Loop Through a Subfile

Process every record in a multi-page list (subfile).

// Read all records from WRKACTJOB
await session.waitForText("Work with Active Jobs", 5000)

const jobs = []
let pageNum = 0

while (true) {
    pageNum++
    session.log("Reading page " + pageNum)

    const screen = await session.readScreen()

    // Read rows 6 through 22 (subfile data area)
    for (let row = 6; row <= 22; row++) {
        const line = screen[row - 1].join("").trim()
        if (line === "" || line.startsWith("===")) continue

        // Parse columns: Name(1-10), User(11-20), Type(21-27), Status(28-35)
        const job = {
            name:   screen[row-1].slice(0, 10).join("").trim(),
            user:   screen[row-1].slice(10, 20).join("").trim(),
            type:   screen[row-1].slice(20, 27).join("").trim(),
            status: screen[row-1].slice(27, 35).join("").trim()
        }
        if (job.name) jobs.push(job)
    }

    // Check for "More..." or "Bottom" indicator
    const lastLine = screen[23].join("")
    if (lastLine.includes("Bottom")) break

    // Page down for more records
    await session.sendKey("pagedown")
    await session.waitForUnlock()
}

session.log("Total jobs found: " + jobs.length)
session.result(jobs)

Recipe 5: Extract Data to JSON

Combine navigation and data extraction to produce a JSON report.

// Generate JSON report of all active MSGW jobs
await session.waitForText("Work with Active Jobs", 5000)

const msgwJobs = []

// Filter for MSGW status only
await session.type("WRKACTJOB STS(*MSGW)")
await session.sendKey("enter")
await session.waitForUnlock()

const screen = await session.readScreen()
for (let row = 6; row <= 22; row++) {
    const status = screen[row-1].slice(27, 35).join("").trim()
    if (status !== "MSGW") continue

    msgwJobs.push({
        jobName: screen[row-1].slice(0, 10).join("").trim(),
        user:    screen[row-1].slice(10, 20).join("").trim(),
        ts:      new Date().toISOString()
    })
}

const report = {
    generatedAt: new Date().toISOString(),
    msgwCount: msgwJobs.length,
    jobs: msgwJobs
}

session.log("MSGW jobs: " + msgwJobs.length)
session.result(report)

// Optional: email alert if any MSGW jobs found
if (msgwJobs.length > 0) {
    session.alert("MSGW Alert: " + msgwJobs.length + " jobs waiting for message reply")
}

Recipe 6: Conditional Navigation

Branch based on screen content.

// Sign on and detect which application loaded
await session.waitForText("Sign On", 5000)
await session.setField("USER", "AUTOUSER")
await session.setField("PASSWORD", "autopass")
await session.sendKey("enter")
await session.waitForUnlock(15000)

// Read the first line to determine current screen
const line1 = (await session.readText(1, 1, 79)).trim()

if (line1.includes("Work with Messages")) {
    // Answer any pending messages first
    session.log("Messages waiting - handling...")
    await session.sendKey("pf3")
    await session.waitForUnlock()
} else if (line1.includes("Display Program Messages")) {
    // Reply with "G" to continue
    await session.type("G")
    await session.sendKey("enter")
    await session.waitForUnlock()
} else if (line1.includes("IBM i Main Menu")) {
    session.log("Clean login, on main menu")
} else {
    session.log("Unknown screen: " + line1)
    throw new Error("Unexpected screen after sign-on: " + line1)
}

Recipe 7: Error Handling

Robust scripts handle host errors and timeouts gracefully.

async function navigateToCustomer(custNo) {
    try {
        await session.waitForText("IBM i Main Menu", 5000)
        await session.type("CALL CUSTMNT PARM('" + custNo + "')")
        await session.sendKey("enter")
        await session.waitForUnlock(15000)

        // Check for error message in OIA/message line
        const msgLine = (await session.readText(24, 1, 79)).trim()
        if (msgLine && !msgLine.startsWith("F3")) {
            throw new Error("Host error: " + msgLine)
        }

        await session.waitForText("Customer Maintenance", 8000)
        return true

    } catch (e) {
        session.log("ERROR: " + e.message)

        // Try to recover
        try {
            await session.sendKey("pf3")
            await session.waitForUnlock(3000)
        } catch (ignored) {}

        return false
    }
}

const ok = await navigateToCustomer("000042")
if (ok) {
    const name = await session.readText(7, 20, 40)
    session.log("Customer name: " + name.trim())
}

Scheduled Scripts

Run a script automatically on a schedule, even when no user is connected.

In config.yaml:

scheduled_scripts:
  - name: daily-msgw-check
    script: /etc/imterm/scripts/check-msgw.js
    schedule: "0 8 * * *"    # 8:00 AM every day
    connection_profile: as400-prod
    auto_connect: true
    output_webhook: "https://slack.corp.com/hooks/xxxxxx"

  - name: weekly-stats-export
    script: /etc/imterm/scripts/export-stats.js
    schedule: "0 6 * * 1"    # 6:00 AM every Monday
    connection_profile: as400-prod
    auto_connect: true
    output_email: "ops@corp.com"

Scheduled scripts auto-connect using the specified profile's saved credentials (stored encrypted in IMTerm's keystore). The script output (from session.result()) is sent to the configured webhook or email.

Via admin UI: Scripts > Scheduled > Add Schedule.


Event-Driven Scripts

Run a script automatically when specific conditions are met.

event_scripts:
  - name: auto-signon
    event: on_connect
    script: /etc/imterm/scripts/auto-signon.js
    connection_profile: as400-prod

  - name: msgw-handler
    event: on_screen
    # Pattern to watch for
    pattern: "Display Program Messages"
    script: /etc/imterm/scripts/handle-msgw.js
    connection_profile: as400-prod

  - name: notify-disconnect
    event: on_disconnect
    script: /etc/imterm/scripts/notify-disconnect.js

Event types: - on_connect - runs immediately when the session connects - on_disconnect - runs when the session disconnects (error or normal) - on_screen - runs when the screen matches pattern


Script Migration: Legacy Emulator Scripts to JavaScript

Convert existing legacy emulator scripts to IMTerm JavaScript.

# Convert a single file
imterm psl2js input.psl -o output.js

# Convert all PSL files in a directory
imterm psl2js --dir /path/to/psl-scripts --out-dir /etc/imterm/scripts

# Preview conversion without writing files
imterm psl2js input.psl --dry-run

What converts automatically: - type "text"session.type("text") - sendkey "enter"session.sendKey("enter") - sendkey "pf3"session.sendKey("pf3") - wait screen "text"session.waitForText("text") - read 5 20 8session.readText(5, 20, 8) - print "message"session.log("message") - if...then...else...endif → JavaScript if - while...wend → JavaScript while - PSL variables → JavaScript const / let

What requires manual review: - Complex PSL string functions (the converter adds a // TODO: review comment) - PSL DDE calls (Windows-specific, no equivalent - document as manual steps) - PSL file I/O (readfile, writefile) → use session.result() instead - PSL network calls → use the REST API for server-side operations

The converted script is wrapped in an async function with proper await calls. Review the // TODO: comments before running in production.


REST API Integration

Call IMTerm's REST API to run scripts or read screen data from external systems.

Run a script via API (Python):

import requests

# Authenticate
r = requests.post("https://imterm.corp.com/api/v2/auth/login",
    json={"username": "api-user", "password": "api-password"})
token = r.json()["token"]

headers = {"Authorization": "Bearer " + token}

# Run a script on an existing session
r = requests.post(
    "https://imterm.corp.com/api/v2/sessions/sess_abc123/script/run",
    headers=headers,
    json={"script": "session.result(await session.readText(1, 1, 79))"}
)
result = r.json()["result"]
print("Screen title:", result)

Read the current screen state:

r = requests.get("https://imterm.corp.com/api/v2/sessions/sess_abc123/screen",
    headers=headers)
screen = r.json()
# screen["rows"] is a 2D array
# screen["fields"] is a list of field objects
# screen["oia"]["connected"] is True/False

Trigger a key press:

r = requests.post(
    "https://imterm.corp.com/api/v2/sessions/sess_abc123/key",
    headers=headers,
    json={"key": "enter"}
)

Full API documentation: see IMTerm-API-Reference.pdf or the built-in Swagger UI at https://imterm.corp.com/api/docs.


Triggers

Triggers combine event detection with automatic actions without writing a script.

In config.yaml:

triggers:
  - name: signon-complete
    pattern: "IBM i Main Menu"
    action: set_title
    params:
      title: "AS/400 - Signed In"

  - name: msgw-alert
    pattern: "Display Program Messages"
    action: notify
    params:
      type: browser_notification
      message: "AS/400 is waiting for your reply"

  - name: error-sound
    pattern: "NAT\\d{4}"    # Natural error message
    action: play_sound
    params:
      sound: error

Trigger actions: set_title, notify, play_sound, run_script, set_status_bar.


Security Notes

  • Scripts run in a V8 isolate with no access to the filesystem or network beyond the session API.
  • Script execution is logged to the audit log (IMTE3001I: script start, IMTE3002I: script complete, IMTE3003W: script error).
  • Credentials stored by session.setField("PASSWORD", ...) are not logged.
  • Only users with the user role or higher can run scripts. View-only users cannot.
  • Scheduled scripts use a dedicated service account credential, separate from interactive users.
  • session.alert() sends a browser notification visible only to the session's owner.

See also: IMTerm Admin Guide section 9 (Scripting), IMTerm-API-Reference.pdf