# 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`:

```lua
-- 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:

```lua
-- 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.

```lua
-- 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.

```lua
-- 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.

```lua
-- 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.

```lua
-- 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.

```lua
-- 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
```