scripting / scripting-5Markdown ↓

Integrations

An integration's scripts run in the Integration profile: the same ctx names as a Normal script, narrowed to the integration. It reaches devices, templates and scripts only through the resources its manifest binds; its cache, config and store belong to the installed instance; and ctx.integration describes the instance itself. The time limit for all of them is the manifest's timeout_ms.

Describing the instance

An integration reads its install-time config with ctx.config.get and its identity and bindings with ctx.integration:

-- In an integration, ctx.config.get reads the instance's install-time config,
-- never your account's: config can carry secrets, and it stays with the instance.
-- With no key it returns the whole table.
local configOk, config = ctx.config.get()
if not configOk then error(config) end

-- An empty config is `{}`, never nil — `config.endpoint` needs no nil guard on
-- the table itself, only on the field.
local endpoint = config.endpoint

-- The installed manifest version, e.g. "1.4.0".
local versionOk, version = ctx.integration.getVersion()
if not versionOk then error(version) end

-- getId is THIS install; getIntegrationId is the definition it was installed
-- from. Two installs of the same integration share the second, never the first.
local idOk, instanceId = ctx.integration.getId()
if not idOk then error(instanceId) end

local definitionOk, integrationId = ctx.integration.getIntegrationId()
if not definitionOk then error(integrationId) end

-- Bindings map a manifest resource name to what the operator wired it to at
-- install time. A bare instance binds nothing, so this is often empty — look a
-- binding up by name and handle nil rather than assuming it is there.
local bindingsOk, bindings = ctx.integration.getBindings()
if not bindingsOk then error(bindings) end

return {
  instanceId = instanceId,
  integrationId = integrationId,
  version = version,
  endpoint = endpoint,
  configKeys = table.size(config),
  bindingCount = table.size(bindings),
}

ctx.integration exists only in integrations. In any other script, using it is an error when the script is checked:

-- ctx.integration exists only in integrations; a Normal script has none.
local ok, id = ctx.integration.getId()
return { id = id }

Lifecycle hooks

An integration's server script puts its lifecycle hooks on exports: exports.init(config) and exports.shutdown(config) as the instance starts and stops, exports.on_timer(name, config) for each timer its manifest declares, and exports.on_event(event, config) for each event it subscribes to. What a hook returns is ignored.

-- An integration's server script puts its lifecycle hooks on `exports`:
-- exports.on_timer(name, config) for each timer its manifest declares,
-- exports.on_event(event, config) for each event it subscribes to, and
-- exports.init(config) and exports.shutdown(config) as the instance starts and
-- stops. What a hook returns is ignored.
exports.on_timer = function(name, config)
  if name ~= "poll-orders" then return end

  local region = config.region or "EU"
  ctx.log.info("polling orders", { region = region })

  -- Keep when this poll ran, in the instance's own store.
  local ok, saved = ctx.store.put("last-poll", { at = time.now():format(), region = region }, "orders")
  if not ok then error(saved) end
end

Handler scripts

Instead of hooks in one server script, an integration can give each timer, event and command its own handler script. The script is the handler: it runs with the hook's arguments, read at the top. An init or shutdown handler receives the config, a timer handler the timer's name and the config, an event handler the event and the config, and a command handler the command's name, its input and the config. What a command handler returns is the command's result.

-- An integration can give a timer, an event or a command its own handler script.
-- The script is the handler: it runs with the hook's arguments, read at the top.
-- A command handler receives the command's name, its input and the instance's
-- config, and what it returns is the command's result.
local name, input, config = ...
if name ~= "lookup-order" then error("unknown command: " .. tostring(name)) end
if type(input) ~= "table" or type(input.orderId) ~= "string" then
  error("lookup-order needs an orderId")
end

-- A missing record is a successful read: ok is true and the record is nil.
local ok, record = ctx.store.get(input.orderId, "orders")
if not ok then error(record) end

return { orderId = input.orderId, found = record ~= nil, region = config.region }

Server functions

An integration's server functions are called by name with the caller's arguments, like a custom app's, and what they return goes back to the caller.

-- An integration's server functions are called by name with the caller's
-- arguments, like a custom app's, and what they return goes back to the caller.
exports.recordShipment = function(orderId, carrier)
  if type(orderId) ~= "string" or orderId == "" then error("an order id is required") end

  local ok, saved = ctx.store.put(orderId, { carrier = carrier, shippedAt = time.now():format() }, "orders")
  if not ok then error(saved) end

  return { orderId = saved.name, carrier = carrier }
end

Workflow actions

An integration can offer actions to workflows. Like a custom workflow action, the action's script receives the node's input and the action's configuration, and what it returns is the node's output.

-- An integration can offer actions to workflows. Like a custom workflow action,
-- the action's script receives the node's input and the action's configuration.
local input, config = ...
local prefix = config.prefix or "ORDER"

local ok, saved = ctx.store.put(prefix .. "-" .. input.orderId, { status = input.status }, "orders")
if not ok then error(saved) end

-- The returned table is the node's output.
return { saved = saved.name, status = input.status }

Upgrades

When an instance moves to a new version of its integration, the upgrade script can carry its config forward. It receives the old version, the new version and the old config. A table it returns becomes the new config; anything else keeps the old config unchanged.

-- When an instance moves to a new version of its integration, the upgrade script
-- can carry its config forward. It receives the old version, the new version and
-- the old config. A table it returns becomes the new config; anything else keeps
-- the old config unchanged.
local oldVersion, newVersion, oldConfig = ...

local config: {[string]: any} = {}
for key, value in pairs(oldConfig) do config[key] = value end

-- Version 2 renamed `url` to `endpoint`.
if config.url ~= nil and config.endpoint == nil then
  config.endpoint = config.url
  config.url = nil
end

ctx.log.info("upgraded config", { from = oldVersion, to = newVersion })
return config

Published platform manual · revision b74f33a56a1d