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 8 → session.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
sessionAPI. - 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
userrole 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