7.3 KiB
WebDev API Contract — CLAUDE.md
Parent: ../CLAUDE.md
Overview
All external interaction with Ignition tags goes through WebDev endpoints. No external tool should read or write tag files directly at runtime. The test runner, CLI tools, and any future integrations use these endpoints.
Base URL
http://<gateway>:8088/system/webdev/<project-name>
Default: http://localhost:8088/system/webdev/framework
Endpoints
POST /tagWrite
Write one or more tag values.
Request:
{
"writes": [
{
"path": "[default]Devices/Pumps/P-101/Command/Start",
"value": true
},
{
"path": "[default]Devices/Pumps/P-101/Command/SpeedSetpoint",
"value": 60.0
}
]
}
Response (200):
{
"status": "ok",
"results": [
{
"path": "[default]Devices/Pumps/P-101/Command/Start",
"success": true
},
{
"path": "[default]Devices/Pumps/P-101/Command/SpeedSetpoint",
"success": true
}
]
}
Response (400 — bad request):
{
"status": "error",
"message": "Invalid tag path: [default]Devices/Pumps/P-999/Command/Start"
}
Handler (Jython 2.7):
# WebDev endpoint: tagWrite
# Method: POST
# Jython 2.7 — no f-strings, no type hints
import json
def doPost(request, session):
logger = system.util.getLogger("webdev.tagWrite")
try:
payload = json.loads(request["data"])
writes = payload.get("writes", [])
if not writes:
return {"status": "error", "message": "No writes provided"}
paths = [w["path"] for w in writes]
values = [w["value"] for w in writes]
results = system.tag.writeBlocking(paths, values)
response_results = []
for i, qv in enumerate(results):
response_results.append({
"path": paths[i],
"success": qv.isGood()
})
return {"status": "ok", "results": response_results}
except Exception as e:
logger.error("tagWrite error: %s" % str(e))
return {"status": "error", "message": str(e)}
GET /tagRead
Read one or more tag values.
Request:
GET /tagRead?paths=[default]Devices/Pumps/P-101/Status/Running,[default]Devices/Pumps/P-101/Status/Speed
Multiple paths are comma-separated in the query string.
Response (200):
{
"status": "ok",
"results": [
{
"path": "[default]Devices/Pumps/P-101/Status/Running",
"value": true,
"quality": "Good",
"timestamp": "2026-03-14T10:30:00Z"
},
{
"path": "[default]Devices/Pumps/P-101/Status/Speed",
"value": 59.8,
"quality": "Good",
"timestamp": "2026-03-14T10:30:00Z"
}
]
}
Handler (Jython 2.7):
# WebDev endpoint: tagRead
# Method: GET
# Jython 2.7 — no f-strings, no type hints
def doGet(request, session):
logger = system.util.getLogger("webdev.tagRead")
try:
paths_param = request["params"].get("paths", "")
if not paths_param:
return {"status": "error", "message": "No paths provided"}
paths = [p.strip() for p in paths_param.split(",")]
qvs = system.tag.readBlocking(paths)
results = []
for i, qv in enumerate(qvs):
results.append({
"path": paths[i],
"value": qv.value,
"quality": str(qv.quality),
"timestamp": str(qv.timestamp)
})
return {"status": "ok", "results": results}
except Exception as e:
logger.error("tagRead error: %s" % str(e))
return {"status": "error", "message": str(e)}
GET /tagBrowse
Browse the tag tree from a root path.
Request:
GET /tagBrowse?root=[default]Devices/Pumps
Response (200):
{
"status": "ok",
"root": "[default]Devices/Pumps",
"children": [
{
"name": "P-101",
"path": "[default]Devices/Pumps/P-101",
"type": "UdtInstance",
"has_children": true
},
{
"name": "P-102",
"path": "[default]Devices/Pumps/P-102",
"type": "UdtInstance",
"has_children": true
}
]
}
Handler (Jython 2.7):
# WebDev endpoint: tagBrowse
# Method: GET
# Jython 2.7 — no f-strings, no type hints
def doGet(request, session):
logger = system.util.getLogger("webdev.tagBrowse")
try:
root = request["params"].get("root", "")
browse_results = system.tag.browse(root)
children = []
for result in browse_results.getResults():
children.append({
"name": result["name"],
"path": str(result["fullPath"]),
"type": str(result["tagType"]),
"has_children": result["hasChildren"]
})
return {"status": "ok", "root": root, "children": children}
except Exception as e:
logger.error("tagBrowse error: %s" % str(e))
return {"status": "error", "message": str(e)}
GET /health
Check gateway and connection status.
Request:
GET /health
Response (200):
{
"status": "ok",
"gateway": {
"state": "RUNNING",
"uptime_ms": 3600000
},
"connections": {
"plc": {
"ControlLogix": {
"connected": true,
"status": "Connected"
}
},
"database": {
"postgres": {
"connected": true,
"status": "Valid"
}
}
}
}
Rules
- All external I/O goes through these endpoints. No exceptions at runtime.
- Never bypass WebDev to edit tag JSON files while the gateway is running.
- Validate inputs in every handler — check for missing fields, bad paths.
- Log errors with
system.util.getLogger()in every handler. - Return consistent JSON — always include
statusfield ("ok" or "error"). - Jython 2.7 only in handler code — no f-strings, no type hints.
Error Handling Convention
All endpoints return this structure on error:
{
"status": "error",
"message": "Human-readable error description"
}
HTTP status codes:
200— success (status: "ok")400— bad request (missing params, invalid paths)500— server error (unhandled exceptions)
Verification Commands
Quick checks from the host or test runner:
# Health check
curl -s http://localhost:8088/system/webdev/framework/health | python3 -m json.tool
# Read a tag
curl -s "http://localhost:8088/system/webdev/framework/tagRead?paths=[default]Devices/Pumps/P-101/Status/Running" | python3 -m json.tool
# Write a tag
curl -s -X POST http://localhost:8088/system/webdev/framework/tagWrite \
-H "Content-Type: application/json" \
-d '{"writes":[{"path":"[default]Devices/Pumps/P-101/Command/Start","value":true}]}' | python3 -m json.tool
# Browse tags
curl -s "http://localhost:8088/system/webdev/framework/tagBrowse?root=[default]Devices" | python3 -m json.tool
File Structure
webdev/
├── CLAUDE.md ← you are here
└── endpoints/
├── tagWrite/
│ ├── code.py ← handler source (Jython 2.7)
│ └── resource.json
├── tagRead/
│ ├── code.py
│ └── resource.json
├── tagBrowse/
│ ├── code.py
│ └── resource.json
└── health/
├── code.py
└── resource.json