<a id="server-lua"></a>

# Server Lua and capabilities

Keep Lua in the workspace's separate server source. Declare exported handlers in
XML with a mode, and give each function one explicitly typed input. Return types
are inferred when omitted. Functions use the existing exports convention. The compiler validates the source contract; the server runs
handlers with the same Lua and the same `ctx` as every other script, and validates their inputs and outputs.

Explicit `input` and `output` contracts can embed a `schema` element in the `urn:cococo:json-schema:1` namespace. Prefer this XML syntax when writing contracts by hand. Existing JSON text and CDATA remain supported. The XML schemas manual in Authoring Docs describes the shared syntax; handler contracts retain the restrictions below.

## Typed handler contracts

A handler without `input`/`output` children derives its contract from a direct,
top-level `exports.name` function declaration in server Lua. Inputs must be explicitly typed object types; inline records and named aliases
are supported. Outputs are inferred from the checked function body unless a
return annotation is supplied. Explicit return annotations are checked. Both
contracts must describe objects. Type aliases, nested records, arrays,
optional values, scalar types, literal enums, and discriminated object unions
are supported. Objects are closed: unknown properties are rejected. Recursive
types, `any`, maps, function-valued fields and ambiguous unions are rejected.
Inference requires a bounded JSON-compatible result: unresolved `any` values
(including untyped empty arrays) need a local or return annotation. GraphQL
results are typed from your instance schema. Use one export assignment per statement when inferring outputs.
Dynamic export registration requires an explicit legacy JSON Schema contract.

`FormResult` is built in: an object containing an `errors` array whose entries
have a required string `message` and optional nullable string `path`. Use
`json.array()` for an empty array and `json.object()` for an empty object: a plain
empty table encodes as `[]`. Wrap computed sequences with `json.array(values)` to
preserve their inferred element type. Do not redeclare `FormResult`.

A handler's input keeps its JSON exactly: a `null` field arrives as `json.null()`
(test it with `json.isnull`), and an absent field is `nil`. Return `json.null()` to
send a `null`.

```xml
<workspace id="typed-form" title="Typed note form" role="Operations" xmlns:t="urn:cococo:dataxml">
  <handler name="validateNote" mode="action"/>
  <panel id="main" title="Note">
    <query type="graphql">query { __typename }</query>
    <form id="note" mode="create">
      <submit type="lua" handler="validateNote" errors="errors"/>
      <field path="body" label="Note" control="textarea" min-length="1" max-length="2000"/>
      <form-actions save-label="Check note"/>
    </form>
  </panel>
</workspace>
```

```lua
type NoteInput = { body: string }

-- This example validates a note; it deliberately performs no database writes.
function exports.validateNote(input: NoteInput): FormResult
  local errors = json.array()
  if input.body:match("^%s*$") then
    errors[1] = {path="body", message="Write a note before saving."}
  end
  return {errors=errors}
end
```

Field restrictions (`required="true"`, `min-length`, `max-length`, `minimum`,
`maximum`, `min-items`, `max-items`) strengthen the Lua handler input contract.
They are enforced by the server on every call, including direct API calls.
Restrictions from all forms using the same handler are combined before any
form is compiled; contradictory bounds are an authoring error. They cannot
weaken a Lua type. Optional parent objects remain optional unless explicitly
required at the handler boundary. Form restrictions apply to ordinary fields,
including fields in sections/fieldsets; variant-case fields continue to take
constraints from their declared handler type/schema.

For arguments without a visible form field, place a `field` child with a `path`
and restrictions inside the `handler`. GraphQL form constraints continue to
come from the GraphQL contract. Explicit JSON Schema input/output pairs remain
supported for existing sources and advanced contracts; supply both or neither.

## Editor assistance

The Workspace editor analyzes XML and Lua together after a short typing pause,
without saving drafts, running Lua, or querying business data. It displays
source-specific diagnostics and supports a manual check with Cmd/Ctrl+Enter in
the Lua editor. XML suggestions come from the generated component catalog.
Lua suggestions include the Workspace API, named types, typed handler inputs,
and inferred expression members, including GraphQL results. Hover shows types/signatures; the outline lists handlers, and definition
navigation connects XML handler references to Lua declarations.

Rename refactoring and project-wide module navigation are not provided. The
runtime still validates every handler call.

## GraphQL results are inferred

A literal GraphQL operation passed to `ctx.graphql.query` is checked against the
current schema. Its selected fields, aliases, fragments, list element types,
nullability and variables determine the Lua call's type. Intermediate result
records do not need handwritten type declarations. Immutable local query strings
are supported; a variable assigned anywhere in the script, including a closure,
falls back to dynamic-query checking. Passing a document through an arbitrary
helper function also uses that fallback. The editor reports the loss of static
operation checking rather than guessing a result shape.

Bind both `ok` and the response. A successful call can still contain GraphQL
errors, absent data or null data: a GraphQL `null` arrives as Lua `nil`, so check
`data`, and the fields you read, before use. A successful call does not prove that
the GraphQL operation succeeded.

Interface and union selections retain their concrete alternatives. Selecting
`__typename` enables narrowing by its literal value. Conditional selections can
be absent even when the schema field is non-null. Variables follow input object,
enum, default and OneOf rules; recursive input schemas use bounded named type
references. Built-in scalars have known wire types. Custom scalars use their
`@iac_scalar(carries:)` metadata; an unmapped scalar remains `any`.

The starter below is checked against your instance schema. Its intermediate GraphQL
responses and the `jobNotes` output contract are inferred. Its input is typed
inline, and `table.map` preserves the note element types while adding presentation
data. The action keeps `FormResult` because its empty error array needs an explicit
element contract. No GraphQL result records are redeclared.

```lua
-- Add a Knowledge Note attached to the selected job; never overwrite its description.
exports.addJobNote = function(input: { entityId: string, body: string }): FormResult
  local body = input.body:match("^%s*(.-)%s*$") or ""
  if body == "" then
    local errors = json.array()
    errors[1] = {path="body", message="Write a note before saving."}
    return {errors=errors}
  end
  local ok, response = ctx.graphql.query([[
    mutation AddJobNote($input: CreateKnowledgeNoteInput!) {
      result: createKnowledgeNote(input: $input) {
        knowledgeNote { id }
        errors { path message }
      }
    }
  ]], {input={kind="ENTITY", entityId=input.entityId, body=body, scope="TENANT"}})
  if not ok then error(response) end
  if response.errors and #response.errors > 0 then error(response.errors[1].message) end
  if not response.data then error("GraphQL returned no data") end
  return {errors=response.data.result.errors}
end

-- Keep storage vocabulary out of the interface.
exports.jobNotes = function(input: { id: string })
  local ok, response = ctx.graphql.query([[
    query JobNotes($id: ID!, $jobId: JobID!) {
      job: getJob(id: $jobId) { id name }
      notes: knowledgeNotes(first: 50, filter: {entityId: $id}, sort: [{field: CREATED_AT, direction: DESC}]) {
        edges { node { id body scope createdAt } }
      }
    }
  ]], {id=input.id, jobId=input.id})
  if not ok then error(response) end
  if response.errors and #response.errors > 0 then error(response.errors[1].message) end
  if not response.data then error("GraphQL returned no data") end
  return {
    job=response.data.job,
    notes={edges=json.array(table.map(response.data.notes.edges, function(edge)
      local note = edge.node
      local visibility = "Everyone"
      if note.scope == "PERSONAL" then visibility = "Only me" end
      return {node={id=note.id, body=note.body, scope=note.scope,
        createdAt=note.createdAt, visibility=visibility}}
    end))},
  }
end
```

The same analyzer checks Lua when you save a Workspace, validate a Script,
validate Lua through MCP, or run IaC checks. It parses and validates operations without executing them, invoking resolvers,
or changing authorization. Dynamic operations remain subject to ordinary runtime
GraphQL validation and permissions. The request supports at most 64 distinct
operations, 256 KiB per operation, 32,768 non-whitespace syntax bytes outside strings/comments, syntax nesting of 64, selection nesting of 32,
and 8,192 expanded selections/types. Exceeding a bound produces a diagnostic.

The editor offers schema-driven completion and hover inside GraphQL query
strings and checker-derived completion and hover on Lua results after guards and
assignments. GraphQL diagnostics point into the original Lua literal, including
escaped strings and long-bracket strings. Completion may be unavailable while
surrounding Lua syntax is incomplete; rename/refactoring is not part of this
service. Handler inputs remain explicitly typed. For a handler declared in XML without
legacy JSON Schema children, the compiler derives its output contract from the
checked return type, including inferred GraphQL result fields.

A read handler produces data. An action handler performs an explicit write or
compound workflow. Separate GraphQL mutations are separate writes: wrapping them
in one Lua handler does not create a database transaction. Use a single domain
mutation when atomicity is required. Compensation is best-effort and can itself
fail; report partial completion rather than claiming rollback. The GraphQL capability runs under the invoking user's
permissions; a handler is not an authorization bypass. Handle transport and GraphQL
errors before interpreting a successful domain payload.

This source implements the customer-workbench contact action. The complete form
and handler contract appear in the [recipe chapter](workspace-manual://workspace-xml-4/recipes).

```lua
local function trim(value)
  return value:match("^%s*(.-)%s*$")
end

-- Compound input rules stay on the server. GraphQL still owns persistence,
-- tenant scoping, resource permissions, and authoritative domain validation.
exports.createContact = function(input)
  local first, last = trim(input.firstName), trim(input.lastName)
  local errors = json.array()
  if first == "" then errors[#errors + 1] = {path="firstName", message="Enter a first name."} end
  if last == "" then errors[#errors + 1] = {path="lastName", message="Enter a last name."} end
  if #errors > 0 then return {record=json.null(), errors=errors} end
  local contact = {customerId=input.customerId, firstName=first, lastName=last}
  if input.email and not json.isnull(input.email) then
    contact.email = trim(input.email):lower()
  end
  local ok, response = ctx.graphql.query([[
    mutation CreateContact($input:CreateContactInput!) {
      result:createContact(input:$input) {
        contact {id firstName lastName email}
        errors {path message}
      }
    }
  ]], {input=contact})
  if not ok then error(response) end
  if response.errors and #response.errors > 0 then error(response.errors[1].message) end
  if not response.data then error("GraphQL returned no data") end
  return {record=response.data.result.contact, errors=response.data.result.errors}
end
```

Return the declared result shape and path-addressed errors expected by the form.
Use the provided JSON helpers when an empty array or JSON null must be preserved
across Lua's table/nil boundary. Do not infer a successful write solely from the
absence of an exception: check the business mutation's returned errors and record.

For read computations, the KPI recipe's handler asks GraphQL for a metric series.
Its output contract covers both empty and populated results.

```lua
local function checked_query(document, variables)
  local ok, response = ctx.graphql.query(document, variables)
  if not ok then error(response) end
  if response.errors and #response.errors > 0 then error(response.errors[1].message) end
  if not response.data then error("GraphQL returned no data") end
  return response.data
end

exports.addContact = function(input)
  local function trim(value) return value:match("^%s*(.-)%s*$") end
  local first, last, email = trim(input.firstName), trim(input.lastName), trim(input.email):lower()
  if first == "" or last == "" then error("First and last names are required") end
  if not email:match("^[^%s@]+@[^%s@]+%.[^%s@]+$") then error("Enter an email address") end
  local data = checked_query([[
    mutation AddContact($input: CreateContactInput!) {
      result: createContact(input: $input) { contact { id } errors { message } }
    }
  ]], {input = {customerId = input.customerId, firstName = first, lastName = last, email = email}})
  if data.result.errors and #data.result.errors > 0 then error(data.result.errors[1].message) end
  if not data.result.contact then error("Contact was not created") end
  return {contactId = data.result.contact.id}
end

exports.qualitySummary = function(input)
  return checked_query([[
    query Quality($id: MetricID!) {
      series: getMetricPoints(metricId: $id, limit: 24) { avg min max count points { value } }
    }
  ]], {id = input.metricId})
end
```

Source text is stored with the authoring artifact. Readers receive the compiled
block definition, not the server Lua. Publication verifies the artifact against
its source. Workspace authoring never runs JavaScript on the server.

## Handler JSON Schema vocabulary

Ordinary schema nodes accept: `additionalProperties`, `const`, `description`, `enum`, `items`, `maxItems`, `maxLength`, `maximum`, `minItems`, `minLength`, `minimum`, `properties`, `required`, `title`, `type`.

Alternative nodes accept only: `description`, `oneOf`, `title`, `type`. `oneOf` requires 2–256 closed object branches, exactly one common required string discriminator, and unique string `const` values. Arbitrary schema unions are unsupported.

Schema nesting is limited to 16 levels. JSON is limited to 256 KiB and 24 levels. `additionalProperties` must be boolean. References, regular expressions, `exclusiveMinimum`, and unlisted keywords are rejected. Use inclusive `minimum`/`maximum` when they express the intended contract; enforce other business rules explicitly in the handler and return field errors.


## Returning display values

The following display handler combines a name and formats a numeric amount with
its currency code. This is display formatting, not money arithmetic or a
locale-aware currency formatter. For schema queries, bind these returned fields
just like ordinary GraphQL response fields.

```xml
<workspace id="formatted-customer" title="Customer summary" role="Sales" xmlns:t="urn:cococo:dataxml">
  <handler name="summary" mode="read">
    <input>
      <schema type="object" xmlns="urn:cococo:json-schema:1">
        <additional-properties boolean="false"/>
        <property name="amount" type="number"/>
        <property name="currency" type="string"/>
        <property name="first" type="string"/>
        <property name="last" type="string"/>
        <required name="first"/>
        <required name="last"/>
        <required name="amount"/>
        <required name="currency"/>
      </schema>
    </input>
    <output>
      <schema type="object" xmlns="urn:cococo:json-schema:1">
        <additional-properties boolean="false"/>
        <property name="balance" type="string"/>
        <property name="name" type="string"/>
        <required name="name"/>
        <required name="balance"/>
      </schema>
    </output>
  </handler>
  <state name="first" default='"Ada"'/><state name="last" default='"Lovelace"'/>
  <state name="amount" default="12.5"/><state name="currency" default='"GBP"'/>
  <panel id="summary" title="Customer">
    <query type="lua" handler="summary"><arg name="first" state="first"/><arg name="last" state="last"/><arg name="amount" state="amount"/><arg name="currency" state="currency"/></query>
    <text id="name" t:text="name"/>
    <text id="balance" t:text="balance"/>
  </panel>
</workspace>
```

```lua
exports.summary = function(input)
  return {
    name = input.first .. " " .. input.last,
    balance = string.format("%.2f %s", input.amount, input.currency)
  }
end
```

Before publishing, check how each business handler behaves with empty results,
server errors and a partial write. A successful save proves the source is valid,
not that the current user has permission for every write, or that separate
mutations happen together.

## Reporting a partial write

This action handler makes two independent writes. When customer creation
succeeds but contact creation fails, the result retains the saved customer ID
and reports partial success. Your workspace should offer recovery against that ID;
blindly retrying the entire handler can create duplicate customers. A network
failure can also leave the outcome unknown, so verify saved records before retrying.
This example has no automatic compensation and promises no rollback.

```xml
<workspace id="partial-write" title="Compound write contract" role="Sales" xmlns:t="urn:cococo:dataxml">
  <handler name="onboard" mode="action">
    <input>
      <schema type="object" xmlns="urn:cococo:json-schema:1">
        <additional-properties boolean="false"/>
        <property name="firstName" type="string"/>
        <property name="lastName" type="string"/>
        <property name="name" type="string"/>
        <required name="name"/>
        <required name="firstName"/>
        <required name="lastName"/>
      </schema>
    </input>
    <output>
      <schema type="object" xmlns="urn:cococo:json-schema:1">
        <additional-properties boolean="false"/>
        <property name="complete" type="boolean"/>
        <property name="customerId" type="string"/>
        <property name="message" type="string"/>
        <required name="complete"/>
        <required name="customerId"/>
        <required name="message"/>
      </schema>
    </output>
  </handler>
  <panel id="explanation" title="Handler fixture"><query type="graphql">query { __typename }</query></panel>
</workspace>
```

```lua
local function record(document, variables, root, field)
  local ok, response = ctx.graphql.query(document, variables)
  if not ok then return nil, "Request failed; verify the saved records before retrying." end
  if response.errors and #response.errors > 0 then return nil, response.errors[1].message end
  local payload = response.data and response.data[root]
  if not payload then return nil, "No mutation result returned." end
  if payload.errors and #payload.errors > 0 then return nil, payload.errors[1].message end
  local value = payload[field]
  if not value then return nil, "No saved record returned." end
  return value, nil
end

exports.onboard = function(input)
  local customer, problem = record([[mutation($input:CreateCustomerInput!){createCustomer(input:$input){customer{id} errors{message}}}]], {input={name=input.name}}, "createCustomer", "customer")
  if not customer then return {complete=false, customerId="", message=problem} end
  local contact, failure = record([[mutation($input:CreateContactInput!){createContact(input:$input){contact{id} errors{message}}}]], {input={customerId=customer.id, firstName=input.firstName, lastName=input.lastName}}, "createContact", "contact")
  if not contact then
    return {complete=false, customerId=customer.id, message="Customer saved; contact not saved: " .. failure}
  end
  return {complete=true, customerId=customer.id, message="Customer and contact saved."}
end
```