> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Template manifest

> Every field of the template: section in lua.skill.yaml and of the manifest an installer reads

The template manifest is the `template:` section of `lua.skill.yaml` that you author, frozen at publish together with the source agent's promoted version, and served to installers as a display-only projection. This page lists every field of the authored section and the served manifest, and summarizes the install result. The lints that guard the fields are on [Lints and consent](/marketplace/lints-and-consent).

*Verified against lua-cli 3.33.0.*

## Sections

`lua marketplace template draft` writes the section and `publish` sends it. Whenever the section exists, all seven authored sections are sent on every publish, and an absent or empty section clears the server's copy. The env contract is not in the file: declare it with `--env-contract "KEY=description"` (`KEY?=` for optional) on `publish`; omitting the flag keeps the previous version's contract.

| Section                    | Shape                         | Written by `draft`                             |
| -------------------------- | ----------------------------- | ---------------------------------------------- |
| `connections`              | list                          | Yes, inferred                                  |
| `personaTemplate`          | object, or omitted            | Yes, from `{{VAR}}` tokens                     |
| `triggerPresets`           | map keyed by trigger key      | Yes, from jobs                                 |
| `paramsMeta`               | map keyed by env contract key | Yes, from the latest contract                  |
| `onInstall`, `onUninstall` | object, or omitted            | Copied from the latest version, never inferred |
| `installPolicy`            | object, or omitted            | No; re-add after every draft                   |

The frozen primitives (skills, webhooks, jobs, preprocessors, postprocessors, triggers, workflows, model) are never authored here.

## connections\[]

One entry per capability the agent plugs into, satisfiable by any platform in its list. Connections the installer already holds are reused; only gaps are requested.

<ParamField path="key" type="string" required>
  Stable identity of the capability. Non-empty and unique; renaming it is remove-plus-add for installers.
</ParamField>

<ParamField path="capability" type="string" required>
  Discovery label, for example `crm`. `platforms` decides what satisfies it.
</ParamField>

<ParamField path="required" type="boolean" required>
  `true` blocks the install until satisfied. `false` is offered and never blocks; primitives bound to a declined connection are installed inert.
</ParamField>

<ParamField path="ownerType" type="'user' | 'org'">
  Which grant may satisfy it. Omit to accept either.
</ParamField>

<ParamField path="displayName" type="string" required>
  Label of the connect prompt. Non-empty.
</ParamField>

<ParamField path="description" type="string" required>
  Why the agent needs it. Non-empty.
</ParamField>

<ParamField path="platforms" type="array" required>
  At least one `{ type, oauthScopes? }`. `type` is an [integration](/concepts/integrations) type such as `hubspot`; types are unique per entry. `oauthScopes` is refused at publish while granted-scope capture rolls out.
</ParamField>

<ParamField path="boundSkills" type="string[]">
  Skill keys in this version that depend on the connection. A multi-platform entry whose bound skill calls platform-native tools is refused; narrow `platforms` or use capability-level access.
</ParamField>

```yaml lua.skill.yaml theme={null}
template:
  connections:
    - key: crm
      capability: crm
      required: true
      displayName: Your CRM
      description: Used to read deals and write follow-up notes.
      platforms:
        - type: hubspot
        - type: salesforce
      boundSkills: [crm-sync]
```

The served manifest enriches each platform with `name`, `authSupport` (`token`, `oauth`, or `both`), and scopes as `{ scope, displayName, description? }`.

## personaTemplate

The [persona](/concepts/persona) with `{{VAR}}` slots, each declared and typed. Installer answers land on the installed agent; the manifest carries only the declarations.

<ParamField path="template" type="string | { base?, voice?, text? }" required>
  The prompt text; the object form matches the persona's channel branches. At least 40 literal characters must remain across branches beside the slots.
</ParamField>

<ParamField path="vars" type="array" required>
  One declaration per slot. Every `{{TOKEN}}` needs an entry and every entry must appear in at least one branch.
</ParamField>

<ParamField path="editable" type="boolean" required>
  `true` lets the installer edit the resolved text; the edit is stored verbatim and wins over substitution.
</ParamField>

Each `vars[]` entry:

<ParamField path="name" type="string" required>
  Matches `[A-Za-z_][A-Za-z0-9_]*`. `persona` is reserved. Must not collide with an env contract key.
</ParamField>

<ParamField path="displayName" type="string" required>
  Form label. Non-empty.
</ParamField>

<ParamField path="description" type="string" required>
  Help text. Non-empty.
</ParamField>

<ParamField path="type" type="'string' | 'number' | 'boolean' | 'enum' | 'model'" required>
  `model` holds a model code; `enum` and `maxLength` are not allowed on it.
</ParamField>

<ParamField path="required" type="boolean" default="false">
  A var that is not required must carry a `default`.
</ParamField>

<ParamField path="default" type="string | number | boolean">
  Must match `type`; for `enum`, a member of the list.
</ParamField>

<ParamField path="enum" type="string[]">
  Required for `type: enum`: unique, non-empty strings. Not allowed on other types.
</ParamField>

<ParamField path="placeholder" type="string">
  Input placeholder, for example `Acme Inc.`.
</ParamField>

<ParamField path="maxLength" type="integer" default="256">
  Strings only; 1 to 2000.
</ParamField>

<ParamField path="showIf" type="{ param: string, equals: string[] }">
  Ask only while `param` (another var or an env contract key) has one of `equals`. A conditional var must carry a `default`. Rules under [showIf rules](/marketplace/lints-and-consent#showif-rules).
</ParamField>

```yaml lua.skill.yaml theme={null}
template:
  personaTemplate:
    template: |
      You are the sales assistant for {{COMPANY_NAME}}. Chase deals in the
      {{PIPELINE}} pipeline and never discount past the agreed limit.
    editable: true
    vars:
      - name: COMPANY_NAME
        displayName: Your company
        description: Used in the agent's replies and signatures.
        type: string
        required: true
        placeholder: Acme Inc.
      - name: PIPELINE
        displayName: Pipeline to work
        description: Which sales pipeline the agent chases.
        type: string
        default: Enterprise
```

## triggerPresets

A map keyed by trigger key covering the version's jobs, webhooks, [triggers](/concepts/triggers), and connection-event triggers. Keys you don't author default to `{ enabled: true }`; connection-event keys are always `enabled: false`.

<ParamField path="<key>.enabled" type="boolean" required>
  Recommended default. The desktop install form asks the installer to confirm each key; a CLI `install` arms the recommended defaults. `true` is refused on a connection-event key.
</ParamField>

<ParamField path="<key>.displayName" type="string">
  Falls back to the primitive's name, then the humanized key.
</ParamField>

<ParamField path="<key>.description" type="string">
  What enabling does.
</ParamField>

<ParamField path="<key>.editableParams" type="array">
  Schedule fields the installer may change. Not allowed on connection-event keys. Every job with a `cron` schedule gets a `preset.timezone` entry at publish whether or not you declare one.
</ParamField>

Each `editableParams[]` entry:

<ParamField path="path" type="string" required>
  `preset.seconds` on an `interval` schedule, `preset.timezone` on a `cron` schedule, or `workflowRef.input.<dotpath>` for a workflow schedule's input. One entry per path.
</ParamField>

<ParamField path="label" type="string" required>
  Form label; a raw path is never shown.
</ParamField>

<ParamField path="help" type="string">
  Help text under the field.
</ParamField>

<ParamField path="unit" type="string">
  Display only, for example `minutes`.
</ParamField>

<ParamField path="min" type="number" default="60">
  For `preset.seconds`: at least 60 and a multiple of 60.
</ParamField>

<ParamField path="max" type="number">
  At least `min`.
</ParamField>

<ParamField path="step" type="number" default="60">
  For `preset.seconds`: a multiple of 60.
</ParamField>

Interval schedules run on whole minutes, so `seconds` is 60 or a multiple of it, both in the frozen job and in the envelope; the frozen schedule must satisfy its own envelope, and a `once` schedule cannot be published. `workflowRef.input.*` entries may also carry `default` and `required`.

```yaml lua.skill.yaml theme={null}
template:
  triggerPresets:
    poll-crm:
      enabled: true
      displayName: Check for new deals
      description: Looks for deals needing a follow-up and drafts one.
      editableParams:
        - path: preset.seconds
          label: Check every
          help: How often the agent looks for new deals.
          unit: minutes
          min: 60
          step: 60
```

## paramsMeta

Display metadata for env contract keys, keyed by the key. Every key must exist in the contract, and once the section is present every contract key needs an entry with a `displayName`.

<ParamField path="<KEY>.displayName" type="string" required>
  Form label. Non-empty.
</ParamField>

<ParamField path="<KEY>.description" type="string">
  Falls back to the contract's description.
</ParamField>

<ParamField path="<KEY>.type" type="'string' | 'number' | 'boolean' | 'enum' | 'model'" required>
  Values are stored as strings; the type drives validation. A `model` value is not checked against the model catalog at install.
</ParamField>

<ParamField path="<KEY>.default" type="string | number | boolean">
  Must match `type`; for `enum`, a member.
</ParamField>

<ParamField path="<KEY>.enum" type="string[]">
  Required for `type: enum`; unique, non-empty strings.
</ParamField>

<ParamField path="<KEY>.placeholder" type="string">
  Input placeholder.
</ParamField>

<ParamField path="<KEY>.maxLength" type="integer">
  Strings only; 1 to 2000.
</ParamField>

<ParamField path="<KEY>.min" type="number">
  Numbers only; finite and not greater than `max`. A numeric `default` must fall inside the range.
</ParamField>

<ParamField path="<KEY>.max" type="number">
  Numbers only; finite.
</ParamField>

<ParamField path="<KEY>.showIf" type="{ param: string, equals: string[] }">
  Ask only while `param` (another env contract key) has one of `equals`. A hidden key is neither required nor validated, and any value for it is cleared.
</ParamField>

Keys under `LUA_TRIGGER_URL__` are reserved: at install the platform writes one such variable per webhook-source trigger, holding that install's trigger URL, and removes it at uninstall.

```yaml lua.skill.yaml theme={null}
template:
  paramsMeta:
    NOTIFY_VIA:
      displayName: Where to post alerts
      type: enum
      enum: [none, slack]
      default: none
    SLACK_CHANNEL:
      displayName: Slack channel
      type: string
      showIf: { param: NOTIFY_VIA, equals: [slack] }
```

## onInstall, onUninstall, installPolicy

`onInstall` and `onUninstall` are `{ tool?, instruction? }` with at least one field; `installPolicy` is `{ perWorkspace: 'single' | 'multiple' }`. Fields, timing, and refusals are on [Lifecycle hooks](/marketplace/lifecycle-hooks).

## Served manifest

Installers read a projection of the frozen version: display metadata and requirements, never code, trigger instructions, or values. Fields are enriched at read time.

<ResponseField name="header" type="object">
  <Expandable title="properties">
    <ResponseField name="id, name, displayName, description, visibility" type="string">Listing metadata.</ResponseField>
    <ResponseField name="version, latestVersion, latestApprovedVersion" type="integer">The version served, the newest published, and the newest approved (absent when none).</ResponseField>
    <ResponseField name="contentHash" type="string">Digest of the frozen content. Echo it on install as `contentHash`; a changed template answers `409 CONTENT_HASH_MISMATCH`.</ResponseField>
    <ResponseField name="agent" type="{ model?: string }">The frozen model code.</ResponseField>
    <ResponseField name="installable" type="boolean">Not deprecated and, for a public template, approved.</ResponseField>
    <ResponseField name="deprecated" type="boolean">Present as `true` when this version blocks fresh installs.</ResponseField>
    <ResponseField name="reconciliation" type="object | null">Per-caller connection status: `summary { satisfied, total }` and, per capability key, `status` (`satisfied`, `multiple-matches`, `insufficient-scope`, `verify-access`, `unmet-required`, `unmet-optional`), `connection`, `candidates`, `missingScopes`, `connectFrom`, `reason`.</ResponseField>
    <ResponseField name="policy" type="object">Present only when the organization's template policy blocks it: `{ blocked: true, reason, message }`.</ResponseField>
    <ResponseField name="channels[], features[]" type="array">Declare-only runtime dependencies: `{ channel | feature, required, displayName?, description? }`.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="skills[]" type="array">
  `key`, `displayName`, `description`, `version`; `source` (`agent` or `marketplace`) with `marketplaceSkillId` and `versionId` when composed from a marketplace skill.
</ResponseField>

<ResponseField name="connections[]" type="array">
  The authored entries with each platform enriched: `{ type, name, authSupport, oauthScopes?: { scope, displayName, description? }[] }`.
</ResponseField>

<ResponseField name="triggers[]" type="array">
  <Expandable title="properties">
    <ResponseField name="key" type="string">The trigger key.</ResponseField>
    <ResponseField name="type" type="'schedule' | 'webhook' | 'connection-event'">Scheduled job, webhook-source trigger, or connection event.</ResponseField>
    <ResponseField name="displayName, description" type="string">Authored preset copy, else the primitive's name and description.</ResponseField>
    <ResponseField name="enabled" type="boolean">The recommended default; always `false` for connection events.</ResponseField>
    <ResponseField name="preset" type="{ type: 'cron' | 'interval', expression?, seconds?, timezone? }">The frozen schedule; schedules only.</ResponseField>
    <ResponseField name="connectionKey" type="string">Connection events only: the capability the event arrives on.</ResponseField>
    <ResponseField name="editableParams" type="array">Verbatim from the preset, plus the automatic timezone entry.</ResponseField>
    <ResponseField name="nextRuns" type="{ at, timezone }[]">Next fire times, computed at read time.</ResponseField>
    <ResponseField name="workflowKey" type="string">Workflow schedules only: the template workflow the schedule starts.</ResponseField>
    <ResponseField name="runAs" type="'installer' | 'system'">Workflow schedules only. `installer` (the default) fires as the installer and deposits one run card in their [Inbox](/reference/sdk/inbox) per fire; `system`, set by the author on the workflow's schedule, fires user-less and unwatched.</ResponseField>
    <ResponseField name="runNote" type="string">The installer-facing sentence for whichever `runAs` applies.</ResponseField>
    <ResponseField name="backfillOnEnable" type="{ maxOccurrences? }">Missed-fire backfill setting frozen on the schedule.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="params[]" type="array">
  <Expandable title="properties">
    <ResponseField name="name" type="string">The env variable name.</ResponseField>
    <ResponseField name="displayName, description" type="string">From `paramsMeta`, else derived from the key and the contract.</ResponseField>
    <ResponseField name="derivedDisplay" type="boolean">Present as `true` only when `displayName` was derived from the key.</ResponseField>
    <ResponseField name="required" type="boolean">From the contract.</ResponseField>
    <ResponseField name="type" type="string">From `paramsMeta`; `string` for derived entries.</ResponseField>
    <ResponseField name="example, default, enum, placeholder, maxLength, min, max, showIf" type="mixed">Passed through when declared.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="personaTemplate" type="{ template, vars[], editable }">
  The authored section; each var carries `name`, `displayName`, `description`, `type`, and any `required`, `default`, `enum`, `placeholder`, `maxLength`, `showIf`.
</ResponseField>

<ResponseField name="workflows[]" type="array">
  Per frozen workflow: `key`, `displayName`, `description?`, `version`, `form` (`graph` or `script`), `stepCount`, `hasApproval`, `hasSchedule`, `hasSignalWait`, `hasJobTier`, and, when set, `jobSecondsMax`, `inputSchema`, `outputSchema`, `budget`, `concurrencyPolicy`, `schedule`, `scheduleInput`, `workspace`, `params`, `outputVisibility`, `envTemplateKeys`, `knowledge`, `replyChannels`, `protocolVersion`, `phases`, `scriptSurface`. Absent on a version without workflows.
</ResponseField>

<ResponseField name="onInstall, onUninstall, installPolicy" type="object">
  Served verbatim so the install and uninstall screens can show them; `onInstall.startWorkflow` serves the workflow key only.
</ResponseField>

## Install result

`lua marketplace template install --json` prints the install result: the ledger row (`templateId`, `agentId`, `orgId`, `installedVersion`, `appliedAgentVersion`, `status` of `installed` or `apply_failed`, `lastError`, `allowCreatorUpdates`, `appliedBy`, `appliedAt`, `installedBy`), the outcome fields `armedTriggers`, `blockedTriggers`, `unwatchedTriggers`, `triggerEndpoints`, `hiddenParamsCleared`, and `personaSkipped`, and the [lifecycle hook](/marketplace/lifecycle-hooks#install-result-fields) outcomes. The field reference, the request bodies, and the one-call deploy result are on the [agent templates API](/reference/rest/agent-templates#install-result).

The install request the CLI sends carries `version`, `envValues` (from `--env-vars`), and `allowCreatorUpdates` only; `contentHash`, `connectionSelections`, `triggerOverrides`, `personaVars`, `editedPersona`, `acceptModifiedPrimitives`, `acceptWithdrawnParams`, and `model` are REST-only fields of the same route, so persona answers, connection choices, trigger overrides, and drift acceptance come from the desktop install form or the [agent templates API](/reference/rest/agent-templates). `envValues` is a request field, never a result field. The publish request carries `sourceAgentVersion`, `changelog`, `envContract`, the seven sections, and `skipAutoApply`; the version response adds `approvalStatus` (public templates), `autoApplyRunId`, and `autoApplyTargetCount` when a fan-out was started.

## See also

* [Publish a template](/marketplace/publish-a-template) — the how-to that fills this section in
* [Lints and consent](/marketplace/lints-and-consent) — every lint code and the consent surface
* [Lifecycle hooks](/marketplace/lifecycle-hooks) — `onInstall`, `onUninstall`, `installPolicy`
* [`lua marketplace`](/reference/cli/marketplace) — `draft`, `publish`, `view --version`
* [`lua.skill.yaml`](/reference/cli/lua-skill-yaml) — the rest of the file
