Advanced Endpoints
Advanced Endpoints let you implement custom, server-side execute-function behavior: a Lua function published on a clean URL path that encapsulates project-specific logic. This page explains how they work, the contract your Lua function receives, and a worked example.
Overview
Advanced Endpoints are built on execfunction. They are not a separate
runtime — the only difference from a classic call is how invocation
metadata is supplied and what the request body is used for.
| Advanced Endpoint path style | Classic execfunction |
|
|---|---|---|
Route |
|
|
Library / function routing |
Encoded in the URL path |
Supplied in query parameters or a JSON envelope |
Context |
Passed through query parameters such as |
Part of the query/envelope metadata |
Request body |
Dedicated to the endpoint payload; uses the content type your endpoint expects |
A JSON invocation document (for the JSON-envelope POST pattern) |
Because the Advanced Endpoint style moves routing into the URL, the request body is free to carry your actual payload in whatever content type the endpoint implementation expects.
The first path segment after /api/v2/execfunction/ is the library
name. Whether a second segment is required depends on how the library is
written (see Choose a library shape):
-
Library returns a table of functions — the second segment names the function to call:
/api/v2/execfunction/corporate/readMachineHistoryHere
corporateis the Lua library andreadMachineHistoryis the function that will be executed. -
Library returns a single function — there is no function segment; the library name alone routes to that function:
/api/v2/execfunction/corporate
Any additional path segments and query parameters are made available to
the function (see req) so you can implement routing logic
inside Lua.
Before You Start
Prefer the Script Model. Store Advanced Endpoint scripts in the Script Model rather than on objects in the hierarchy. The Script Model and script libraries stored in the object hierarchy are two different mechanisms.
Understand ctx. If a script is stored in the object hierarchy — for
example on a GenFolder in the I/O model — the default context path
configured on the WebServer object may not match the intended execution
context. In that case, provide the ctx query parameter to select a
different execution context.
-
If the relevant scripts are available under the default context path configured on the WebServer object,
ctxis not required. -
If scripts are distributed across different objects in the hierarchy, provide
ctxin the URL to select the desired execution context.
Keep in mind that changing ctx does not mean the logic runs on the
node that owns that object.
Name the script in its first line. Begin each script with a comment
holding the script (library) name, for example -- api-hello. The
WebStudio Config app property editor shows the first line of the script
as the script property, so this header lets you confirm at a glance that
the stored script is the one you designed — and quickly spot a script
that ended up in the wrong place.
Authorize the request. Include authorization information in the request headers according to your Web API authentication setup.
Choose a library shape. A script library can be implemented in two ways, and the choice determines the URL:
-
Return a Lua table containing one or more functions. The caller selects a function with a second path segment:
/api/v2/execfunction/<library>/<function>. -
Return a single function directly. No function segment is needed — the library name alone routes to it:
/api/v2/execfunction/<library>.
Function Signature
Functions invoked through an Advanced Endpoint receive three parameters
— arg, req, and hlp:
function lib:myfunction(arg, req, hlp)
Declare the function with colon notation. With colon notation Lua
passes self implicitly, so the full logical signature is
self, arg, req, hlp. In Lua, lib:myfunction(arg) is the same as
lib.myfunction(lib, arg).
arg
The content of arg depends on the HTTP request body.
-
Empty body:
argis an empty Lua table. -
JSON object body: each property becomes a key in the Lua table.
-
Any other non-empty body:
argbecomes a table with a single key,data, containing the raw request body.
JSON object body:
{
"starttime": "2019-04-02T10:00:00.000Z",
"limit": 35
}
Lua view:
{
["starttime"] = "2019-04-02T10:00:00.000Z",
["limit"] = 35
}
Text body:
Hello World
Lua view:
{
data = "Hello World"
}
JSON array body:
[
"Inmation", "OT", "Data", "Fabric"
]
Lua view:
{
data = { "Inmation", "OT", "Data", "Fabric" }
}
req
req is a Lua table describing the HTTP request and execution context.
| Field | Description |
|---|---|
|
Optional payload of the provided JSON Web Token. |
|
HTTP request headers. |
|
HTTP method, for example |
|
The path segment after the function name. If none is provided, this is an empty string. |
|
Query parameters as a Lua table. If a parameter appears more than once, the value is a Lua string array. |
|
The client IP address. |
|
Object id of the WebServer object that processes the request. |
|
Context information including |
|
Full URL information including |
Use req.path and req.query to implement routing logic inside Lua.
For example, given:
/api/v2/execfunction/corporate/readMachineData/lab/1234?mode=full
readMachineData is the executed function and req.path is
/lab/1234. The values in req.path and req.query are unescaped.
hlp
hlp is a helper object that exposes convenience functions commonly
used by Advanced Endpoints. Use hlp:createResponse when you need to
control the returned body, error payload, HTTP status code, or response
headers.
-
checkPermission -
createResponse -
getProfileNames -
isAdmin -
isAdminOrReviewer -
isJsonNull -
isMemberOfProfile -
isReviewer
checkPermission
Use checkPermission(pathspec, sec_attr, verbose) to validate whether
the current user has the required permissions.
| Parameter | Description |
|---|---|
|
Object path, object reference, or numeric object/property id. |
|
Bitwise OR combination of security attributes. |
|
Optional flag controlling error detail. |
createResponse
Use createResponse(data, error, status_code, headers) to build a
custom HTTP response.
| Parameter | Description |
|---|---|
|
Value or body returned by the endpoint. |
|
Custom error payload. |
|
HTTP status code. |
|
Response headers. |
If Content-Type is set in headers, the data value drives the
response body. If no content type is specified, an error payload takes
precedence when provided.
Example
This is a simple example of an Advanced Endpoint.
Store this Lua snippet as api-hello on a Script Library Item in the
Script Model.
-- api-hello
local lib = {}
function lib:say_hello(arg)
local name = arg.name or 'unknown'
local msg = string.format("Hello %s", name)
return msg
end
return lib
The function is declared with colon notation, so it receives self
implicitly along with arg.
Advanced Endpoint path style — library and function routing live in the URL path:
-
Method:
POST -
Request URL:
/api/v2/execfunction/api-hello/say_hello
Request body:
{
"name": "Ben"
}
Response — Content-Type text/plain:
"Hello Ben"
Example Bank
Read Actual Value
Store this Lua snippet as api-hello on a Script Library Item in the
Script Model. If already created, overwrite it with the script below.
-- api-hello
local lib = {}
function lib:say_hello(arg, req, hlp)
local name = arg.name or 'unknown'
local msg = string.format("Hello %s", name)
return msg
end
function lib:getvalue(arg, req, hlp)
local path = arg.path
local value = nil
if path then
value = syslib.getvalue(path, {
prp_val_nil_as_null = true,
error_strategy = 'nil'
})
end
return {
msg = self:say_hello(arg, req, hlp),
value = value
}
end
return lib
The function returns a plain Lua table, so the endpoint serializes it to
JSON automatically and responds with Content-Type: application/json —
you do not need hlp:createResponse for the common case. Reach for
createResponse only when you need a different content type, status
code, or response headers (see the CSV Response
example).
-
Method:
POST -
Request URL:
/api/v2/execfunction/api-hello/getvalue
Request body:
{
"name": "Ben",
"path": "/IO-Home/Examples/Demo Data/Process Data/DC0815"
}
Response — Content-Type application/json:
{
"msg": "Hello Ben",
"value": 14.958015345935872
}
CSV Response
Advanced Endpoints are not limited to JSON. This example exports aligned historical data for several items as a CSV download. It shows three techniques worth reusing:
-
Compose server-side with
performaction. The endpoint builds aconsolidateaction pipeline that reads historical data for each item and aligns the values on a shared timestamp, then runs it in a single call — no client-side stitching of multiple reads. -
Transform the result in Lua. The aligned payload is turned into CSV rows with
esi-mapreduce(map/reduce): one header line followed by one line per interval, with the timestamp converted to ISO 8601 viasyslib.gettime. -
Control the response content type with
hlp:createResponse. Returning the CSV string with aContent-Type: text/csvheader makes the caller receive a CSV document rather than a JSON-wrapped value. This is the key difference from the previous example, which returned a plain Lua table and was serialized as JSON.
Store this Lua snippet as api-csv-exporter on a Script Library Item
in the Script Model.
-- api-csv-exporter
local inAPI = require('syslib.api')
local MR = require('esi-mapreduce')
local map = MR.map
local reduce = MR.reduce
local itemDefList = {
{
name = "DC0815",
path = "/IO-Home/Examples/Demo Data/Process Data/DC0815"
},
{
name = "FC0815",
path = "/IO-Home/Examples/Demo Data/Process Data/FC0815"
},
{
name = "LC0815",
path = "/IO-Home/Examples/Demo Data/Process Data/LC0815"
}
}
local function getActionPipeline(starttime, endtime)
return {
["type"] = "consolidate",
["starttime"] = starttime,
["endtime"] = endtime,
["mode"] = "align",
["items"] = map(itemDefList, function(itemDef)
return {
["name"] = itemDef.name,
["outerJoin"] = true,
["action"] = {
["type"] = "read-historical-data",
["objspec"] = itemDef.path,
["numberOfIntervals"] = 10
}
}
end)
}
end
local lib = {}
---Exports historical data for specified items in CSV format.
---@param arg { starttime: string, endtime: string }
---@param req SysLib.API.Req
---@param hlp SysLib.API.Hlp
function lib:export(arg, req, hlp)
local starttime = arg.starttime
local endtime = arg.endtime
local actionPipeline = getActionPipeline(starttime, endtime)
local output = inAPI:performaction({
action = actionPipeline
})
-- Result is a table with the following structure:
--[=[
{
"payload": [
{
"DC0815": 7.392681985576574,
"FC0815": 38.947148370234174,
"LC0815": 7827.927777973474,
"t": 1780308000000
},
...
]
}
]=]
-- Let's convert the payload to CSV format
-- First the header
local header = map(itemDefList, function(itemDef) return '"' .. itemDef.name .. '"' end)
table.insert(header, 1, '"Timestamp"') -- Add the timestamp column at the beginning
local csvLines = reduce(output.payload, function(acc, row)
local csvLine = map(itemDefList, function(itemDef)
return row[itemDef.name] or ""
end)
local isoTS = syslib.gettime(row.t) -- Convert timestamp to ISO 8601 format
table.insert(csvLine, 1, '"' .. isoTS .. '"') -- Add the timestamp at the beginning of the line
table.insert(acc, table.concat(csvLine, ",")) -- Add the line as a CSV line to the accumulator
return acc
end, {
table.concat(header, ",") -- Add the header line to csvLines
})
local csvResult = table.concat(csvLines, "\n") -- Join all lines with newline characters
local respHeader = {
["Content-Type"] = "text/csv"
}
return hlp:createResponse(csvResult, nil, 200, respHeader)
end
return lib
-
Method:
POST -
Request URL:
/api/v2/execfunction/api-csv-exporter/export
Request body:
{
"starttime": "2026-06-01T10:00:00.000Z",
"endtime": "2026-06-01T10:10:00.000Z"
}
Response — Content-Type text/csv:
"Timestamp","DC0815","FC0815","LC0815"
"2026-06-01T10:00:00.000Z",7.3926819855765737,38.947148370234174,7827.9277779734739
"2026-06-01T10:01:00.000Z",7.3926819855765737,38.947148370234174,7827.9277779734739
"2026-06-01T10:02:00.000Z",7.3581843217877791,38.282693521966472,7925.4648197289243
"2026-06-01T10:03:00.000Z",7.4169059273742795,37.968567011845543,7928.2033615618138
"2026-06-01T10:04:00.000Z",7.4169059273742795,37.968567011845543,7928.2033615618138
"2026-06-01T10:05:00.000Z",7.5675904860405367,37.904043781454952,8073.1551419058515
"2026-06-01T10:06:00.000Z",7.6175959135489917,37.274615032346,8270.1216216287266
"2026-06-01T10:07:00.000Z",7.7954935853757732,37.87439894189508,8417.9627637799149
"2026-06-01T10:08:00.000Z",7.7954935853757732,37.87439894189508,8417.9627637799149
"2026-06-01T10:09:00.000Z",7.6780406972507134,37.606963538303027,8359.2323332829274
Echo
The echo endpoint is a diagnostic tool: it returns whatever arg and
req it received, so you can inspect exactly what an Advanced Endpoint
function is handed for any request you send. Use it to see how a request
body maps to arg, how extra path segments and query parameters land in
req.path and req.query, and what your token payload and headers look
like in req.
It also demonstrates the single-function library shape: the script
returns one function directly, so the URL has no function segment —
echo is the library name and the trailing /info/hello%20to%20you
becomes req.path.
Store this Lua snippet as echo on a Script Library Item in the
Script Model.
-- echo
return function(arg, req)
return {
arg = arg,
req = req
}
end
-
Method:
POST -
Request URL:
/api/v2/execfunction/echo/info/hello%20to%20you?name=inmation&path=%2FHome
Request body:
{
"msg": "Hello World"
}
Response — Content-Type application/json. The endpoint echoes the
arg it derived from the request body and the full req context:
{
"arg": {
"msg": "Hello World"
},
"req": {
"access_token_payload": {
"exp": 1783001253,
"iat": 1783000953,
"in_prf": [
"<USERNAME / PROFILE>"
],
"in_usr": "019ef5d8-3759-72a1-8062-53f1a844709a",
"nbf": 1783000953,
"sub": "<USERNAME>"
},
"context_info": {
"ctx": "/Home/WebServer",
"execfunction": {
"lib": "echo"
},
"topic": "execfunction"
},
"headers": {
"Content-Length": "28",
"Host": "<HOSTNAME>",
"accept": "*/*",
"accept-encoding": "gzip, deflate, br",
"content-type": "application/json"
},
"method": "POST",
"path": "/info/hello to you",
"query": {
"name": "inmation",
"path": "/Home"
},
"remote_ip_address": "<IP-ADDRESS>",
"server_oid": "019ef5d8-386e-708f-8036-d6c96fb60a72",
"url": {
"hostname": "<HOSTNAME>",
"href": "<BASEURL>/api/v2/execfunction/echo/info/hello%20to%20you?name=inmation&path=%2FHome",
"pathname": "/api/v2/execfunction/echo/info/hello to you",
"port": 8008,
"protocol": "http:",
"search": "?name=inmation&path=%2FHome"
}
}
}