From ignition-scada
References Ignition system.* API (14 modules, 239 functions) for Jython 2.7 scripts in SCADA projects. Provides resource.json templates for all resource types like scripts, views, and queries.
npx claudepluginhub thethoughtagen/ignition-ide-plugins --plugin ignition-scadaThis skill uses the workspace's default tool permissions.
You are writing code for **Ignition SCADA** by Inductive Automation. Scripts run in **Jython 2.7** (Python 2.7 on JVM). The scripting API is `system.*`.
Hardens process historian servers (OSIsoft PI, Honeywell PHD, GE Proficy, AVEVA Historian) in OT environments with Purdue network placement, DMZ replication via data diodes/PI connectors, access controls, SQL injection prevention, and data integrity protection.
Hardens historian servers (OSIsoft PI, Honeywell PHD, GE Proficy, AVEVA) in OT environments: Purdue-level network placement, access controls, DMZ replication via data diodes/connectors, SQL injection prevention, data integrity safeguards.
Provides Frappe Framework server-side Python patterns and templates for controllers, document events, whitelisted APIs, background jobs, and database operations. Use for server logic, APIs, events, and data processing.
Share bugs, ideas, or general feedback.
You are writing code for Ignition SCADA by Inductive Automation. Scripts run in Jython 2.7 (Python 2.7 on JVM). The scripting API is system.*.
Every file or directory you create inside an Ignition project MUST have a resource.json. Without it, the gateway silently ignores the resource — no error, no warning, it simply doesn't exist at runtime. This is the #1 cause of "not found" errors when managing Ignition projects via git.
This applies to ALL resource types:
| Resource type | Where | resource.json goes |
|---|---|---|
| Script modules | ignition/script-python/my_package/ | Next to code.py in every directory in the path |
| Script sub-packages | ignition/script-python/my_package/sub/ | In sub/ too — every level needs one |
| Test modules | ignition/script-python/pkg/__tests__/ | In both pkg/ AND __tests__/ |
| Perspective views | com.inductiveautomation.perspective/views/MyView/ | Next to view.json |
| Named queries | com.inductiveautomation.naming/queries/MyQuery/ | Next to query.json |
| Vision windows | com.inductiveautomation.vision/windows/MyWindow/ | Next to window.json |
| WebDev endpoints | com.inductiveautomation.webdev/resources/my-endpoint/ | Next to doGet.py, config.json, etc. |
| Alarm pipelines | com.inductiveautomation.alarm-notification/pipelines/ | Next to pipeline resource files |
| Tag configs | tags/ | Alongside tag JSON exports |
Template for script resources (scope A = script module):
{
"scope": "A",
"version": 1,
"restricted": false,
"overridable": true,
"files": ["code.py"],
"attributes": {
"lastModification": {
"actor": "external",
"timestamp": "2026-01-01T00:00:00Z"
}
}
}
Template for Perspective views (scope G = general):
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": ["view.json", "thumbnail.png"],
"attributes": {
"lastModification": {
"actor": "external",
"timestamp": "2026-01-01T00:00:00Z"
},
"lastModificationSignature": "unknown"
}
}
Template for WebDev endpoints:
{
"scope": "G",
"version": 1,
"restricted": false,
"overridable": true,
"files": ["config.json", "doGet.py", "doPost.py"],
"attributes": {
"lastModification": {
"actor": "external",
"timestamp": "2026-01-01T00:00:00Z"
}
}
}
The files array must list every file in the directory that Ignition should load. If you add a file but don't list it in files, Ignition ignores it.
When creating any new resource in an Ignition project: ALWAYS create the resource.json at the same time. Never create a code.py, view.json, or any other Ignition resource file without its accompanying resource.json.
Ignition's script library (ignition/script-python/) has a strict structure. Each directory is either a leaf module or a package node — never both.
Leaf module — has code.py with real code, NO child directories with code:
core/util/secrets/
├── code.py ← actual functions live here
└── resource.json
Package node — has child directories, NO code.py (or only an empty placeholder):
core/util/
├── secrets/ ← child module
│ ├── code.py
│ └── resource.json
├── csv/ ← child module
│ ├── code.py
│ └── resource.json
└── resource.json ← resource.json still required, but NO code.py
NEVER put a code.py in a directory that also contains child packages. Ignition treats a directory with code.py as a leaf module. If you also put subdirectories in it, the behavior is undefined and the child modules may not be importable.
Exception: __tests__/ directories are special — Ignition's script runtime ignores directories whose names start with __, so they do not conflict with a sibling code.py. The testing framework relies on this convention.
WRONG:
my_package/
├── code.py ← has real code
├── resource.json
└── utils/ ← real child package — conflicts with code.py above
├── code.py
└── resource.json
OK (special case):
my_package/
├── code.py ← has real code
├── resource.json
└── __tests__/ ← ignored by Ignition runtime, used by test framework
├── code.py
└── resource.json
CORRECT (general pattern):
my_package/
├── resource.json ← package node, no code.py
├── logic/
│ ├── code.py ← real code lives in a leaf
│ └── resource.json
└── __tests__/
├── code.py ← tests live in a leaf
└── resource.json
When resource.json exists without code.py, Ignition recognizes the directory as a package node. The files array in resource.json should be empty or omit code.py:
{
"scope": "A",
"version": 1,
"restricted": false,
"overridable": true,
"files": [],
"attributes": {
"lastModification": {
"actor": "external",
"timestamp": "2026-01-01T00:00:00Z"
}
}
}
This project uses ignition-lint to catch Ignition-specific issues. The auto-lint hook runs on every file write, but you should also be aware of what it checks so you write clean code from the start:
print() function form, not print xfrom module import *system.* function names are correctsystem.* with local variablesnow() defaults to 1-second polling; use now(5000) or higherIf ignition-lint is installed, the plugin's auto-lint hook catches these automatically after every file edit. If it's not installed, the hook silently exits — nothing breaks, but you lose automatic feedback.
print() function form (not print x)from java.util import ArrayListsystem.util.getLogger() for logging, not print()runPrepQuery/runPrepUpdate with ? params — NEVER string-format SQLacknowledge, cancel, createRoster, getRosters, getShelvedPaths, listPipelines, queryJournal, queryStatus, shelve, unshelve
addColumn, addRow, addRows, appendDataset, clearDataset, dataSetToHTML, deleteRow, deleteRows, exportCSV, exportExcel, exportHTML, filterColumns, formatDates, fromCSV, getColumnHeaders, setValue, sort, toCSV, toDataSet, toExcel, toPyDataSet, updateRow
now, parse, format, midnight, setTime, addHours, addMinutes, addSeconds, addMillis, addDays, addWeeks, addMonths, addYears, getHour12, getHour24, getMinute, getSecond, getMillis, getYear, getMonth, getDayOfMonth, getDayOfWeek, getDayOfYear, getAMorPM, getDate, getTimezone, getTimezoneOffset, getTimezoneRawOffset, hoursBetween, minutesBetween, secondsBetween, millisBetween, daysBetween, monthsBetween, yearsBetween, toMillis, fromMillis, isAfter, isBefore, isBetween, isDaylightTime
runQuery, runPrepQuery, runUpdateQuery, runPrepUpdate, beginTransaction, commitTransaction, rollbackTransaction, closeTransaction, createSProcCall, execSProcCall
fileExists, getTempFile, openFile, openFiles, readFileAsBytes, readFileAsString, saveFile, writeFile
messageBox, warningBox, errorBox, confirm, inputBox, passwordBox, chooseColor, color, openDesktop, closeDesktop, desktop, getCurrentDesktop, getDesktopHandles, getWindow, findWindow, getWindowNames, getOpenedWindows, getOpenedWindowNames, getParentWindow, getSibling, getQuality, transform, convertPointToScreen, createPopupMenu, getScreenIndex, setScreenIndex, getScreens, isTouchscreenModeEnabled, setTouchscreenModeEnabled, showNumericKeypad, showTouchscreenKeyboard, openDiagnostics
centerWindow, closeParentWindow, closeWindow, desktop, getCurrentWindow, goBack, goForward, goHome, openWindow, openWindowInstance, swapTo, swapWindow
httpGet, httpPost, httpPut, httpDelete, httpClient, openURL, sendEmail, getHostName, getIpAddress, getRemoteServers
browse, browseServer, browseSimple, getServers, getServerState, isServerEnabled, readValue, readValues, setServerEnabled, writeValue, writeValues
sendMessage, print, navigate, openPopup, closePopup, togglePopup, getSessionInfo, getProjectInfo, openDock, closeDock, toggleDock, refresh, setTheme, vibrate, navigateForward, navigateBack, download
getRoles, getUsername, getUserRoles, isScreenLocked, lockScreen, logout, switchUser, unlockScreen, validateUser
readBlocking, readAsync, writeBlocking, writeAsync, exists, queryTagHistory, browse, configure, getConfiguration, deleteTags, copy, move, rename, exportTags, importTags, query, browseHistoricalTags, queryTagCalculations, storeTagHistory, requestGroupExecution
addCompositeSchedule, addHoliday, addRole, addSchedule, addUser, createScheduleAdjustment, editHoliday, editRole, editSchedule, editUser, getHoliday, getHolidayNames, getHolidays, getNewUser, getRoles, getSchedule, getScheduledUsers, getScheduleNames, getSchedules, getUser, getUsers, getUserSources, isUserScheduled, removeHoliday, removeRole, removeSchedule, removeUser
getLogger, jsonDecode, jsonEncode, sendMessage, invokeAsynchronous, invokeLater, execute, getSystemFlags, getGlobals, setGlobals
# Tag reads — always list form, returns QualifiedValue list
values = system.tag.readBlocking(["[default]Path/To/Tag"])
val = values[0].value
# Parameterized queries — ALWAYS use runPrepQuery
results = system.db.runPrepQuery(
"SELECT * FROM table WHERE id = ? AND status = ?",
[myId, myStatus], "DatabaseName"
)
# Dataset iteration
for row in range(ds.getRowCount()):
name = ds.getValueAt(row, "name")
# Logging
logger = system.util.getLogger("MyModule")
logger.info("Processing %s items" % count)
# Perspective messaging
system.perspective.sendMessage("updateChart", {"tagPath": path}, scope="page")
? paramssystem variableimport *getSibling()/getParent() for component references