Files

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

  1. All external I/O goes through these endpoints. No exceptions at runtime.
  2. Never bypass WebDev to edit tag JSON files while the gateway is running.
  3. Validate inputs in every handler — check for missing fields, bad paths.
  4. Log errors with system.util.getLogger() in every handler.
  5. Return consistent JSON — always include status field ("ok" or "error").
  6. 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