> ## 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 Reference

> Every attribute of an Agent Template manifest — authored, frozen, and served — with types, defaults, constraints, and the publish lints that guard them

This is the complete attribute reference for Agent Template manifests. For the concepts and lifecycle, read [Agent Templates](/marketplace/agent-templates); for the authoring journey, [Publishing Templates](/marketplace/publishing-templates); for the installer's experience, [Deploying Templates](/marketplace/deploying-templates).

## Where the manifest lives

A template's manifest exists in three forms. Knowing which form an attribute belongs to tells you where you set it, where it's stored, and where an installer reads it:

1. **The authored sections** — what you write. The `template:` section of your project's `lua.skill.yaml` carries the four authored sections (`connections`, `personaTemplate`, `triggerPresets`, `paramsMeta`). The publish request can additionally carry sections that have no yaml home yet: the env contract (declared with `--env-contract`), connection-event triggers (`declaredTriggers`), `outcomes`, `channels`, `features`, and marketplace-skill composition refs. The publish request also carries three version-level fields: `sourceAgentVersion` (which promoted agent version to freeze; defaults to the active one), `changelog`, and `referenceEvalRunId` (the eval run satisfying the publish gate, when an eval set exists).
2. **The frozen published version** — what publish produces. An immutable, integer-numbered version that combines your promoted agent's primitives (skills, webhooks, jobs, processors, triggers, model — frozen **by value**) with your authored sections (validated, normalized, and stored alongside), plus derived integrity digests: `contentHash`, and the `codeHash` / `consentSurfaceHash` pair that powers update diffing and re-consent (creator-side version reads only — the two are never in the public manifest).
3. **The served manifest** — what installers read. `GET /marketplace/templates/:id/versions/:v/manifest` serves a **safe projection** of the frozen version: display metadata and requirements only, never source code, trigger instructions, or values. Fields are lifted by an explicit allow-list — anything not deliberately made public never serves. The serving layer also *enriches* it at read time: platform display names and auth modes from the live integration catalog, scope display copy, next run times, the per-caller `reconciliation` block, and the org `policy` block.

| Attribute group                                                                    | Authored by you                   | Frozen at publish                                | In the served manifest                                                             |
| ---------------------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------- |
| Metadata (`name`, `displayName`, …)                                                | at `create` / `patch`             | header, not per-version                          | yes                                                                                |
| `agent.model`, `agent.modelSettings`                                               | no — frozen from the source agent | yes                                              | `model` only                                                                       |
| `personaTemplate`                                                                  | yes (yaml)                        | yes (normalized)                                 | yes — the template text is part of the consent surface                             |
| `skills[]` (and webhooks, jobs, processors, triggers)                              | no — frozen from the source agent | yes, by value                                    | display metadata only (skills; schedule/webhook triggers)                          |
| `connections[]`                                                                    | yes (yaml)                        | yes                                              | yes, platform entries enriched                                                     |
| `triggerPresets`                                                                   | yes (yaml)                        | yes (normalized over the full trigger key-space) | merged into `triggers[]`                                                           |
| `declaredTriggers[]`                                                               | yes (publish request)             | yes                                              | display fields only — `event` and `instruction` never serve                        |
| `envContract` + `paramsMeta`                                                       | yes (`--env-contract` + yaml)     | yes                                              | merged into `params[]`                                                             |
| `channels[]`, `features[]`                                                         | yes (publish request)             | yes                                              | yes                                                                                |
| `outcomes`                                                                         | yes (publish request)             | yes                                              | creator/version reads and outcome summaries (not the deploy-screen manifest in V1) |
| `contentHash`, `installable`, `deprecated`, `reconciliation`, `policy`, `nextRuns` | never                             | derived                                          | yes — served-only, see [below](#served-manifest-only-fields)                       |

**Inheritance vs. clearing.** On the publish API, an *omitted* authored section inherits the previous version's (re-validated against the new content); an explicit empty value (`[]`, `{}`, `{ units: [] }` for `outcomes`, or `null` for `personaTemplate`) **clears** it. The CLI is stricter: when a `template:` section exists in `lua.skill.yaml`, all four yaml sections are always sent — an absent or empty subsection is an explicit clear, never "keep the previous version's". Omitting `--env-contract` inherits the previous contract.

**Keys are identity.** Every keyed entry — a skill key, connection key, trigger key, param name, persona var name, outcome unit key — is its stable identity across versions. Renaming a key means *remove + add*: installers lose their binding, toggle, or setting for the old key. Keep keys stable.

***

## metadata

Template-level, not per-version: set at `lua marketplace template create`, editable later (except `name`).

| Field                                 | Type                  | Required | Default   | Constraints                                   | Description                                                                                      |
| ------------------------------------- | --------------------- | -------- | --------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `name`                                | string                | yes      | —         | unique per creator organization               | Machine name of the template, e.g. `sales-followup`.                                             |
| `displayName`                         | string                | yes      | —         | —                                             | Catalog and deploy-screen title.                                                                 |
| `description`                         | string                | no       | `''`      | —                                             | Catalog card copy.                                                                               |
| `visibility`                          | `public` \| `private` | no       | `private` | flipping to `public` enqueues platform review | Private templates are org-only; public templates list in the catalog once a version is approved. |
| `id`                                  | string (UUID)         | derived  | —         | server-assigned                               | Opaque template id, no prefix.                                                                   |
| `listed`                              | boolean               | derived  | `true`    | —                                             | `false` after unlisting (soft retire).                                                           |
| `latestVersion`                       | integer               | derived  | —         | monotonic                                     | Highest published version number.                                                                |
| `latestApprovedVersion`               | integer               | derived  | —         | absent = no approved version                  | Highest *approved* version — what public installers resolve "latest" to.                         |
| `installCount` / `activeInstallCount` | integer               | derived  | `0`       | —                                             | Lifetime installs / currently-installed count.                                                   |

<Note>
  Versions are **integers** (v1, v2, v3 — not semver), monotonic per template. A rejected publish burns no version number. Catalog categories and icons are not part of the V1 manifest.
</Note>

## agent

| Field             | Type   | Required | Default | Constraints                                                               | Description                                                                                                              |
| ----------------- | ------ | -------- | ------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `model`           | string | frozen   | —       | validated against the installer org's model catalog at install pre-flight | The model frozen from the source agent, e.g. `anthropic/claude-sonnet-4`. The only `agent` field in the served manifest. |
| `modelSettings`   | object | frozen   | —       | —                                                                         | The source agent's model settings, frozen by value. Applied at install; not served in the public manifest.               |
| `personaTemplate` | object | no       | absent  | see below                                                                 | The persona section — authored, not frozen from the source agent.                                                        |

### agent.personaTemplate

The system prompt as a template with `{{VAR}}` slots, each declared, typed, and explained. Authored in the yaml `template:` section.

| Field      | Type                                 | Required | Default | Constraints                                                      | Description                                                                                                                                          |
| ---------- | ------------------------------------ | -------- | ------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `template` | string \| `{ base?, voice?, text? }` | yes      | —       | must keep ≥ 40 literal (non-slot) characters across all branches | The prompt text. The object form matches the persona's channel-aware branches.                                                                       |
| `vars`     | array                                | yes      | —       | exact bijection with `{{TOKEN}}`s (see below)                    | One declaration per `{{slot}}`.                                                                                                                      |
| `editable` | boolean                              | yes      | `false` | enforced server-side at install                                  | When `true`, the installer may edit the resolved prompt at deploy; the edited text wins over substitution and is stored verbatim, never re-expanded. |

Each entry of `vars[]`:

| Field         | Type                                        | Required          | Default | Constraints                                                                                                       | Description                                         |
| ------------- | ------------------------------------------- | ----------------- | ------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `name`        | string                                      | yes               | —       | `[A-Za-z_][A-Za-z0-9_]*`; `persona` is reserved (case-insensitive); must not collide with any declared param name | The `{{TOKEN}}` this declares.                      |
| `displayName` | string                                      | yes               | —       | non-empty                                                                                                         | Form label — the installer never sees a bare token. |
| `description` | string                                      | yes               | —       | non-empty                                                                                                         | Help copy under the field.                          |
| `type`        | `string` \| `number` \| `boolean` \| `enum` | yes               | —       | —                                                                                                                 | Drives the input control and value validation.      |
| `required`    | boolean                                     | no                | `false` | a non-required var **must** carry a `default`                                                                     | Whether the installer must answer.                  |
| `default`     | string \| number \| boolean                 | no                | —       | must satisfy its own `type`; for `enum`, must be a member                                                         | Pre-filled answer.                                  |
| `enum`        | string\[]                                   | when `type: enum` | —       | non-empty, unique, strings; illegal on other types                                                                | The choices.                                        |
| `placeholder` | string                                      | no                | —       | —                                                                                                                 | Input placeholder, e.g. `Acme Inc.`.                |
| `maxLength`   | integer                                     | no                | `256`   | strings only; 1–2000                                                                                              | Length cap on the supplied value (injection floor). |

**Substitution rules** (what happens to `{{vars}}` at deploy):

* **Single-pass.** The template is scanned once; each declared token is replaced with the installer's answer. Substituted values are never re-scanned — a value containing `{{OTHER_VAR}}` lands as inert text, not a new slot.
* **Brace-escaped.** Inside supplied values, `{{` becomes `{ {` and `}}` becomes `} }` — a value can never mint a slot or reach runtime substitution targets.
* **The bijection lint.** Every `{{TOKEN}}` in any branch (`base`/`voice`/`text`) must have a `vars[]` entry, and every declared var must appear in at least one branch. Publish rejects both directions.
* **Values are never stored in the manifest.** Answers land on the installed agent's persona; the manifest carries only the declarations.

```yaml theme={null}
template:
  personaTemplate:
    template: |
      You are the sales assistant for {{COMPANY_NAME}}. Chase deals in the
      {{PIPELINE}} pipeline. Never discount past {{MAX_DISCOUNT}}%.
    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
      - name: MAX_DISCOUNT
        displayName: Max discount %
        description: The agent never offers more than this.
        type: number
        default: 15
```

## skills\[]

Skills (like webhooks, jobs, processors, and code triggers) are **frozen from your promoted agent version, by value** — you never author these entries. The served manifest lifts display metadata only; source code, tool schemas, and gating conditions never serve publicly. Webhooks and processors serve nothing in the V1 manifest at all — they're internal wiring, not an installer decision surface.

Served shape per entry:

| Field                | Type                     | Required                   | Default            | Constraints                                                       | Description                                                                               |
| -------------------- | ------------------------ | -------------------------- | ------------------ | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `key`                | string                   | always                     | —                  | stable identity; rename = remove + add                            | The skill's name on the source agent.                                                     |
| `displayName`        | string                   | always                     | derived from `key` | —                                                                 | The skill's title; when none was set, the key is humanized (`crm-sync` → `Crm sync`).     |
| `description`        | string                   | always                     | `''`               | —                                                                 | "What this agent can do" copy on the deploy screen.                                       |
| `version`            | string                   | always                     | —                  | —                                                                 | The frozen skill version.                                                                 |
| `source`             | `agent` \| `marketplace` | when present               | `agent`            | —                                                                 | Provenance: frozen from the source agent, or composed from a published marketplace skill. |
| `marketplaceSkillId` | string                   | `source: marketplace` only | —                  | —                                                                 | The referenced marketplace skill's id.                                                    |
| `versionId`          | string                   | `source: marketplace` only | —                  | an immutable version-document id — never a semver, never "latest" | The exact pinned marketplace skill version.                                               |

Marketplace composition is authored on the publish request as `marketplaceSkills: [{ marketplaceSkillId, versionId }]` (max 50) — content is still frozen by value into the version; the refs record where it came from and pin the exact version.

## connections\[]

Declared runtime connection requirements — "this agent needs a CRM" — authored in the yaml `template:` section. Each entry is a **capability** satisfiable by any platform in its allow-list.

| Field         | Type            | Required | Default | Constraints                                                            | Description                                                                                                                                                                      |
| ------------- | --------------- | -------- | ------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `key`         | string          | yes      | —       | non-empty, unique; stable identity                                     | The capability key. Referenced by `boundSkills`-owning outcomes evidence (`match.connectionKey`), connection-event triggers (`connectionKey`), and the install's binding ledger. |
| `capability`  | string          | yes      | —       | must equal the **primary** catalog category of every declared platform | Discovery label ("crm", "calendar", "chat"). The `platforms[]` allow-list, not this label, is what decides satisfiability.                                                       |
| `required`    | boolean         | yes      | —       | —                                                                      | `true`: install blocks until satisfied. `false`: offered at deploy, never blocking — primitives depending on a declined optional connection are materialized inert, fail-closed. |
| `ownerType`   | `user` \| `org` | no       | either  | —                                                                      | Constrains which grant identity may satisfy the capability — declare `org` when the connection must be the organization's (finance systems), never someone's personal key.       |
| `displayName` | string          | yes      | —       | non-empty                                                              | The connect chip label ("Your CRM").                                                                                                                                             |
| `description` | string          | yes      | —       | non-empty                                                              | The *why*, shown under the chip ("Used to read deals and write follow-up notes.").                                                                                               |
| `platforms`   | array           | yes      | —       | at least one; unique `type`s                                           | The ANY-OF allow-list — any one platform satisfies the capability.                                                                                                               |
| `boundSkills` | string\[]       | no       | —       | every entry must be a skill key in this version                        | Which of the template's skills depend on this connection. Drives the portability lint and outcome attribution.                                                                   |

Each entry of `platforms[]`:

| Field         | Type      | Required            | Default | Constraints                                                    | Description                                                                                                                                                                                                    |
| ------------- | --------- | ------------------- | ------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`        | string    | yes                 | —       | must be a live integration in the catalog                      | Integration type, e.g. `salesforce`, `hubspot`, `slack`.                                                                                                                                                       |
| `oauthScopes` | string\[] | for OAuth platforms | —       | scope names must exist in the platform's scope-copy vocabulary | The scopes this template needs on that platform — what the installer consents to. **Currently disabled at publish** while granted-scope capture rolls out; a declaration is rejected with a message saying so. |

**In the served manifest**, each platform entry is enriched from the live integration catalog at read time — the frozen declaration stores only `{type, oauthScopes}`:

```json theme={null}
{ "type": "salesforce", "name": "Salesforce", "authSupport": "oauth",
  "oauthScopes": [
    { "scope": "crm_contact_read", "displayName": "Read contacts" },
    { "scope": "crm_deal_write", "displayName": "Create & edit deals" } ] }
```

Raw scope tokens never reach a screen — `oauthScopes` (and `missingScopes` in reconciliation) always serve as `{scope, displayName, description?}` objects with platform-owned display copy.

**Reconciliation semantics, per attribute.** At deploy, the platform reconciles each capability against the connections the installer already holds (personal + the target org's shared ones): a connection whose `type` is in `platforms[]` and whose granted scopes cover the declared `oauthScopes` → `satisfied`, bound without asking; several matches → `multiple-matches`, the installer picks (never auto-picked); a match lacking scopes → `insufficient-scope` with the exact missing scopes; a pre-scope-capture row whose live scope query failed → `verify-access` (informational, resolved at pre-flight); no match → `unmet-required` (blocks) or `unmet-optional` (offered). `ownerType` filters which grants qualify at all. Install proceeds only when every `required` capability is satisfied — re-verified server-side, never trusted from the client.

**The portability lint.** A capability may list *multiple* platforms only when its `boundSkills` reach the connection through the capability itself. A bound skill that calls platform-native tools (e.g. `salesforce_*`-prefixed calls, or a platform-typed integration helper) pins the entry: publish rejects with a migration hint — narrow `platforms[]` to that platform, or migrate the skill to capability-level access. Single-platform entries lint nothing; narrowing is the author's escape hatch.

```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: salesforce
        - type: hubspot
      boundSkills: [crm-sync]
    - key: notify
      capability: chat
      required: false
      displayName: Notifications channel
      description: "Optional: where follow-up alerts get posted."
      platforms:
        - type: slack
```

## triggerPresets and the served triggers\[]

Schedules and code triggers are frozen from the source agent; **triggerPresets** is your authored layer over them — recommended defaults, installer-facing copy, and which schedule fields the installer may edit. It's a map keyed by trigger key, covering the union of the version's job, webhook, code-trigger, and connection-event keys. Keys you don't author get defaults at publish: `{ enabled: true }` for job/webhook/code-trigger keys, always-off for connection-event keys.

| Field            | Type    | Required | Default                                                           | Constraints                                                                    | Description                                                                                                                                                             |
| ---------------- | ------- | -------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`        | boolean | yes      | `true` (schedule/webhook) / `false` (connection-event, immutable) | connection-event keys may never author `true`                                  | A **recommended default**, pre-checked in the configure step — confirmed at deploy, never silently applied. Nothing arms without the installer's explicit confirmation. |
| `displayName`    | string  | no       | the primitive's name, else humanized key                          | —                                                                              | Deploy-screen title for the automation.                                                                                                                                 |
| `description`    | string  | no       | the primitive's description                                       | —                                                                              | What enabling *does* ("Looks for deals needing a follow-up and drafts one.").                                                                                           |
| `editableParams` | array   | no       | —                                                                 | not allowed on connection-event keys; timezone auto-declared on every cron job | Which schedule fields the installer may adjust.                                                                                                                         |

Each entry of `editableParams[]`:

| Field   | Type                                  | Required | Default | Constraints                                                                                                            | Description                                                                                              |
| ------- | ------------------------------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `path`  | `preset.seconds` \| `preset.timezone` | yes      | —       | closed allow-list; `preset.seconds` only on interval presets, `preset.timezone` only on cron; one declaration per path | The schedule field this exposes. Cron *expressions* are never editable — no client-side cron math, ever. |
| `label` | string                                | yes      | —       | non-empty                                                                                                              | Form label ("Check every") — a raw path is never rendered.                                               |
| `help`  | string                                | no       | —       | —                                                                                                                      | Copy under the field.                                                                                    |
| `unit`  | string                                | no       | —       | display only                                                                                                           | e.g. `minutes`.                                                                                          |
| `min`   | number                                | no       | `60`    | ≥ 60 and a multiple of 60 (`preset.seconds`)                                                                           | Lower bound.                                                                                             |
| `max`   | number                                | no       | —       | ≥ `min`                                                                                                                | Upper bound.                                                                                             |
| `step`  | number                                | no       | `60`    | multiple of 60 (`preset.seconds`)                                                                                      | Input step.                                                                                              |

**The whole-minutes constraint.** Interval schedules run on whole minutes only: `seconds` must be ≥ 60 and a multiple of 60, both in the frozen schedule and in any authored envelope — a fractional-minute interval would silently never fire, so publish rejects it. Frozen `once` schedules are rejected too (a frozen instant is stale by construction), and a frozen schedule must satisfy its *own* declared envelope.

**The timezone override.** Cron presets carry an IANA timezone — but a frozen cron carries the *creator's* timezone, so every cron job automatically gets a `preset.timezone` editable param (label "Timezone", defaulting to the installer's profile timezone) even if you don't declare one. A Stockholm-authored 08:00 job must not run at Stockholm time for a Nairobi installer.

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

**The served `triggers[]`** merges the frozen schedules, your presets, and the declared connection-event triggers into one list:

| Field                         | Type                                                           | Present               | Description                                                                                                                                                                  |
| ----------------------------- | -------------------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `key`                         | string                                                         | always                | The trigger key.                                                                                                                                                             |
| `type`                        | `schedule` \| `webhook` \| `connection-event`                  | always                | Scheduled job, webhook-source code trigger, or connection event.                                                                                                             |
| `displayName` / `description` | string                                                         | always / when set     | Authored preset copy, falling back to the primitive's name/description, then the humanized key.                                                                              |
| `enabled`                     | boolean                                                        | always                | The recommended default. Connection-event entries always serve `false`.                                                                                                      |
| `preset`                      | `{ type: cron \| interval, expression?, seconds?, timezone? }` | schedule only         | The frozen schedule.                                                                                                                                                         |
| `connectionKey`               | string                                                         | connection-event only | The capability the event arrives on.                                                                                                                                         |
| `editableParams`              | array                                                          | when declared         | Verbatim from the authored preset (plus the auto-declared timezone param).                                                                                                   |
| `nextRuns`                    | `{ at, timezone }[]`                                           | schedule only         | The next fire times — **computed at read time by the serving layer**, never authored, so the UI can say "runs 2:00 AM New York — 8:00 AM your time" without doing cron math. |

At install, the deploy request answers this section with `triggerOverrides: { <key>: { enabled?, params? } }` — e.g. `params: { "preset.seconds": 600, "preset.timezone": "Africa/Nairobi" }`.

## declaredTriggers\[]

Connection-event triggers — "when a deal is won" — are **authored declarations** (publish request only; no yaml home in V1), since no frozen primitive can carry them. Each materializes at install as a managed trigger plus a webhook subscription on the bound connection.

| Field              | Type                                | Required | Default | Constraints                                                                                                                                        | Description                                                                                         |
| ------------------ | ----------------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `key`              | string                              | yes      | —       | shares the one trigger key-space (clash-checked)                                                                                                   | Stable identity.                                                                                    |
| `connectionKey`    | string                              | yes      | —       | must be a declared `connections[]` key                                                                                                             | Which capability's bound connection emits the event.                                                |
| `displayName`      | string                              | yes      | —       | non-empty                                                                                                                                          | Deploy-screen title ("When a deal is won").                                                         |
| `description`      | string                              | yes      | —       | non-empty                                                                                                                                          | What enabling does.                                                                                 |
| `event.objectType` | string                              | yes      | —       | provider object vocabulary                                                                                                                         | e.g. `deal`.                                                                                        |
| `event.event`      | `created` \| `updated` \| `deleted` | yes      | —       | —                                                                                                                                                  | The change type.                                                                                    |
| `event.filters`    | object (string → string)            | no       | —       | —                                                                                                                                                  | Field filters, e.g. `{ "stage": "won" }`.                                                           |
| `instruction`      | string                              | yes      | —       | non-empty, ≤ 2000 chars, must not interpolate event-payload fields (the fencing lint); part of the consent surface — changing it forces re-consent | The instruction prepended to the fired agent turn ("A deal was won — send a thank-you follow-up."). |

**The event-support join.** Each declared `{objectType, event}` must be deliverable on **every** platform in the referenced capability's `platforms[]` — checked at publish against the live webhook-event listing, with each `filters` key limited to that event's available filters on that platform. A listing fetch failure fails the publish loudly (`could not verify event support — try again`) — never publish-unverified.

Two fences apply everywhere:

* **Never armed by default.** A connection-event trigger always serves (and materializes) disabled; authoring `enabled: true` in its preset is rejected. The installer must switch it on explicitly.
* **`event` and `instruction` never serve publicly.** The served manifest lifts only `key`, `connectionKey`, `displayName`, `description` (as a `triggers[]` entry with `type: "connection-event"`). Event payloads are untrusted input at runtime and instruction text is consent-surface material — it's shown through consent flows, not the anonymous catalog.

## envContract and params

The **declare-vs-supply** rule: the manifest declares *which* environment variables the template's code expects — names and metadata, **never values**. Values are supplied per install and stored on the agent.

`envContract` — keyed by variable name, declared with `--env-contract "KEY=description"` (required) or `"KEY?=description"` (optional):

| Field         | Type    | Required | Default | Constraints                                                 | Description                         |
| ------------- | ------- | -------- | ------- | ----------------------------------------------------------- | ----------------------------------- |
| `description` | string  | yes      | —       | —                                                           | What the variable is for.           |
| `required`    | boolean | yes      | —       | missing required values block the install with a named list | Whether the install must supply it. |
| `example`     | string  | no       | —       | never a real value                                          | Illustrative example.               |

`paramsMeta` — the typed display layer over the contract, authored in yaml, keyed by the same names. Keys must be a subset of `envContract`'s — and supplying a `paramsMeta` section at all makes coverage total: every contract key must then carry an entry with a `displayName` (rejection: `<KEY>: user-facing params require displayName`). Key-derived display names appear only on versions published without any `paramsMeta` section (API/legacy publishers), so a served manifest never mixes authored and derived entries:

| Field                                            | Type                                        | Required | Default                                  | Constraints                                                               | Description                                                                   |
| ------------------------------------------------ | ------------------------------------------- | -------- | ---------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `displayName`                                    | string                                      | yes      | —                                        | non-empty                                                                 | Form label.                                                                   |
| `description`                                    | string                                      | no       | falls back to the contract's description | —                                                                         | Help copy.                                                                    |
| `type`                                           | `string` \| `number` \| `boolean` \| `enum` | yes      | —                                        | env values are stored as strings; the type drives validation and coercion | Input type.                                                                   |
| `default` / `enum` / `placeholder` / `maxLength` | —                                           | no       | as for persona vars                      | same typed-declaration rules as persona vars                              | One shared vocabulary — persona vars and params render as one configure form. |

**The served `params[]`** merges both, one entry per contract key:

| Field                                                    | Type    | Description                                                                                       |
| -------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------- |
| `name`                                                   | string  | The env variable name.                                                                            |
| `displayName`                                            | string  | From `paramsMeta`; without it, derived from the key (`CRM_DEFAULT_OWNER` → `Crm default owner`).  |
| `derivedDisplay`                                         | boolean | Present (`true`) only when the display name was key-derived — omitted when authored metadata won. |
| `description`                                            | string  | `paramsMeta` description, falling back to the contract's.                                         |
| `required`                                               | boolean | Always from the contract.                                                                         |
| `type`                                                   | string  | From `paramsMeta`; `string` for derived entries.                                                  |
| `example`, `default`, `enum`, `placeholder`, `maxLength` | —       | Passed through when present.                                                                      |

```yaml theme={null}
template:
  paramsMeta:
    CRM_DEFAULT_OWNER:
      displayName: Default deal owner
      description: Fallback owner when a deal has none.
      type: string
    TONE:
      displayName: Tone of voice
      type: enum
      enum: [formal, casual]
      default: casual
```

## channels\[] and features\[]

**Declare-only runtime dependencies** — things a deploy can never create but the agent needs to fully go live. Declaring them doesn't bind anything; it drives the honest **post-deploy setup checklist** ("connect WhatsApp to go live") and installer health, instead of a false "live". Authored on the publish request.

`channels[]` — messaging/voice channels the template sends or receives on:

| Field         | Type    | Required | Default | Constraints                                                                                                                                                                                                             | Description                                                                                           |
| ------------- | ------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `channel`     | string  | yes      | —       | one of the known channel vocabulary: `whatsapp`, `instagram`, `facebook`, `webchat`, `slack`, `email`, `front`, `messagebird`, `phone`, `teams`, `rcs`, `imessage`, `telegram`, `google`, `meeting`; unique per version | Channel type the agent depends on.                                                                    |
| `required`    | boolean | yes      | —       | —                                                                                                                                                                                                                       | `true`: the agent can't do its job without it — checklist blocks "fully live". `false`: nice-to-have. |
| `displayName` | string  | no       | —       | ≤ 120 chars, non-empty when present                                                                                                                                                                                     | Checklist row title.                                                                                  |
| `description` | string  | no       | —       | ≤ 2000 chars                                                                                                                                                                                                            | Why the agent needs it.                                                                               |

`features[]` — platform feature flags the agent depends on (e.g. the RAG feature for a docs-answering template):

| Field                         | Type    | Required | Default | Constraints                                                    | Description                        |
| ----------------------------- | ------- | -------- | ------- | -------------------------------------------------------------- | ---------------------------------- |
| `feature`                     | string  | yes      | —       | must exist in the platform feature catalog; unique per version | Feature name.                      |
| `required`                    | boolean | no       | `false` | —                                                              | Absent = informational dependency. |
| `displayName` / `description` | string  | no       | —       | same limits as channels                                        | Checklist copy.                    |

<Warning>
  A WhatsApp declaration deserves a scope note in your description: outbound message-template assets are WABA-scoped and need Meta approval — expect days, not minutes, for outreach beyond the 24-hour window.
</Warning>

## outcomes

The template's countable **units of work** — "follow-ups sent", "meetings booked" — declared at publish (publish request only in V1). Counting is **platform-attested**: a unit is recorded only when the platform itself observes the declared evidence — a matching tool call executed on the install's *bound* connection for the named capability, or a platform-recorded event (message delivery, sandbox commerce writes, completed voice calls). Creator code can cause real side effects but cannot mint a count, evidence on an unbound or declined capability is inert rather than counted, and each recorded unit settles when its window passes without reversal — with verification (sampled LLM-judge review before a unit counts as `verified`) owned entirely by the platform: `verification`, `samplingRate`, and `judge` are rejected as creator fields.

`outcomes.units[]` (max 25 per version):

| Field         | Type                                            | Required                                          | Default | Constraints                              | Description                                                                                                                                                           |
| ------------- | ----------------------------------------------- | ------------------------------------------------- | ------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `key`         | string                                          | yes                                               | —       | unique, non-empty; stable identity       | The unit's id, e.g. `followup_sent`.                                                                                                                                  |
| `title`       | string                                          | yes                                               | —       | non-empty                                | Display title ("Follow-ups sent").                                                                                                                                    |
| `description` | string                                          | no                                                | —       | —                                        | What counts as one.                                                                                                                                                   |
| `unitLabel`   | string                                          | no                                                | —       | —                                        | Display noun for one counted unit, e.g. `booking`.                                                                                                                    |
| `match`       | object                                          | at least one of `match` / `events`                | —       | see below                                | Tool-call evidence on a bound connection.                                                                                                                             |
| `events`      | array                                           | at least one of `match` / `events`                | —       | see below                                | Platform-recorded event evidence.                                                                                                                                     |
| `dedupe`      | `thread` \| `occurrence` \| `event` \| `entity` | yes                                               | —       | —                                        | How repeat evidence collapses into counts: one per conversation, per job run, per external event, or per external entity (a batch job posting 20 invoices counts 20). |
| `window`      | string                                          | no                                                | `24h`   | `<n>h` or `<n>d`, between `1h` and `30d` | Settlement window — a recorded unit settles when it passes without reversal.                                                                                          |
| `entityPath`  | string                                          | required for `dedupe: entity` with a tool matcher | —       | dot-path starting `input.` or `result.`  | Where in the tool dispatch the external entity id lives. Optional on other modes (enables entity-precise reversal).                                                   |
| `reversals`   | array                                           | no                                                | —       | see below                                | Evidence that *un-counts* a prior unit.                                                                                                                               |

`match` — tool-call evidence:

| Field           | Type      | Required | Constraints                                       | Description                                                                                                                           |
| --------------- | --------- | -------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `tools`         | string\[] | yes      | at least one; ≤ 128 chars each; `*` wildcard only | **Bare** tool-name globs, matched with the runtime server-name prefix stripped (`crm_update_deal`, `calendar_event_*`).               |
| `connectionKey` | string    | yes      | must be a declared `connections[]` key            | The capability the call must have executed on — the unit records only when the dispatch ran on the install's bound connection for it. |

`events[]` — platform-recorded event evidence (a **closed** vocabulary; anything else is rejected at publish):

| Field     | Type   | Required                    | Constraints                                                                       | Description                    |
| --------- | ------ | --------------------------- | --------------------------------------------------------------------------------- | ------------------------------ |
| `event`   | string | yes                         | one of `message.sent`, `order.created`, `basket.checked_out`, `call.completed`    | The recordable platform event. |
| `channel` | string | required for `message.sent` | delivery-emitting channels only (`whatsapp` today); illegal on non-channel events | The emitting channel.          |

`reversals[]` — each entry needs at least one evidence source:

| Field               | Type    | Required                             | Constraints                                | Description                                                                                             |
| ------------------- | ------- | ------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `match`             | object  | one of `match` / `onDeliveryFailure` | same shape and rules as the unit's `match` | A tool call that signals the outcome was undone (a deal reopened).                                      |
| `onDeliveryFailure` | boolean | one of `match` / `onDeliveryFailure` | —                                          | Reverse on delivery failure (bounce) of the unit's thread.                                              |
| `entityPath`        | string  | no                                   | `input.` / `result.` path                  | Which entity the reversal targets; absent → the unit's most recent recorded outcome on the same thread. |

<Note>
  Reversals are **platform-owned**, never installer-toggleable — declining a trigger must not silently disable clawback while units keep settling. And the lifecycle counters (`recorded` → `settled` → `verified` / `reversed`) are served as separate tiers, never summed: only `verified` may feed pricing or catalog badges.
</Note>

## Served-manifest-only fields

These exist only in the served manifest — derived or attached at read time, never authored:

| Field                       | Type             | Description                                                                                                                                                                                                     |
| --------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version` / `latestVersion` | integer          | The version served / the newest published version (resolved from version records, never a counter).                                                                                                             |
| `contentHash`               | string           | sha256 over the canonical frozen content. Echo it back on install as the **hash-seal**: pre-flight re-verifies the content still matches what you consented to, and answers `409 CONTENT_HASH_MISMATCH` if not. |
| `installable`               | boolean          | The derived install verdict: the version is not deprecated, and — for public templates — approved.                                                                                                              |
| `latestApprovedVersion`     | integer          | Highest approved version; absent when none is.                                                                                                                                                                  |
| `deprecated`                | boolean          | Present (`true`) when this version is deprecated — blocks *new* installs of it only.                                                                                                                            |
| `reconciliation`            | object \| `null` | The per-caller connection reconciliation, pre-computed at read time (below). `null` for anonymous callers.                                                                                                      |
| `policy`                    | object           | Present only when the resolved org's template policy blocks this template: `{ blocked: true, reason, message }` — `reconciliation` stays `null` for a blocked template.                                         |

### The reconciliation block

```json theme={null}
"reconciliation": {
  "summary": { "satisfied": 3, "total": 4 },
  "capabilities": {
    "crm": { "status": "satisfied",
      "connection": { "id": "conn_x", "integrationType": "salesforce", "ownerType": "user" } },
    "calendar": { "status": "multiple-matches",
      "candidates": [
        { "id": "conn_a", "integrationType": "googlecalendar",
          "platformDisplayName": "Google Calendar", "ownerType": "user",
          "accountLabel": "stefan@heylua.ai", "default": true },
        { "id": "conn_b", "integrationType": "googlecalendar",
          "platformDisplayName": "Google Calendar", "ownerType": "org",
          "accountLabel": "Acme (org)", "default": false } ] },
    "email": { "status": "insufficient-scope",
      "connection": { "id": "conn_y", "integrationType": "gmail", "ownerType": "user" },
      "missingScopes": [ { "scope": "email_message_write", "displayName": "Send email" } ] },
    "notify": { "status": "unmet-optional", "connectFrom": ["slack"] }
  }
}
```

Per capability key:

| Field           | Type                                  | Description                                                                                                                                                                                                              |
| --------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `status`        | enum                                  | `satisfied` · `multiple-matches` · `insufficient-scope` · `verify-access` · `unmet-required` · `unmet-optional`.                                                                                                         |
| `connection`    | `{ id?, integrationType, ownerType }` | The matched connection. `id` is redacted on rows the caller may see but not use.                                                                                                                                         |
| `candidates`    | array                                 | On `multiple-matches`: full objects (`id`, `integrationType`, `platformDisplayName`, `ownerType`, `accountLabel?`, `default`) — the picker renders from these fields alone, defaulting per the deploy target's identity. |
| `missingScopes` | array                                 | On `insufficient-scope`: the missing scopes as enriched `{scope, displayName}` objects — never raw tokens.                                                                                                               |
| `connectFrom`   | string\[]                             | On unmet statuses: the platform types the installer may connect.                                                                                                                                                         |
| `reason`        | string                                | Machine-readable detail, e.g. `scope-query-failed` behind `verify-access`.                                                                                                                                               |

Every install-time rejection (a 409 on the deploy exchange) carries a **freshly recomputed** reconciliation block — the UI never needs a blind re-fetch.

***

## A complete example

The authored `template:` section of `lua.skill.yaml` for a Sales Follow-up agent — all four yaml sections:

```yaml theme={null}
template:
  personaTemplate:
    template: |
      You are the sales assistant for {{COMPANY_NAME}}. Chase deals in the
      {{PIPELINE}} pipeline. Never discount past {{MAX_DISCOUNT}}%.
    editable: true                      # installer may tweak the resolved prompt
    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             # optional vars must carry a default
      - name: MAX_DISCOUNT
        displayName: Max discount %
        description: The agent never offers more than this.
        type: number
        default: 15

  connections:
    - key: crm                          # stable identity — rename = remove + add
      capability: crm                   # discovery label; platforms[] decides
      required: true                    # install blocks until satisfied
      displayName: Your CRM
      description: Used to read deals and write follow-up notes.
      platforms:                        # ANY ONE satisfies the capability
        - type: salesforce
        - type: hubspot
      boundSkills: [crm-sync]           # these skills must use capability-level
                                        # access, or publish narrows the list
    - key: notify
      capability: chat
      required: false                   # offered at deploy, never blocking
      displayName: Notifications channel
      description: "Optional: where follow-up alerts get posted."
      platforms:
        - type: slack

  triggerPresets:
    poll-crm:                           # a frozen interval job on the agent
      enabled: true                     # recommended default — confirmed at deploy
      displayName: Check for new deals
      description: Looks for deals needing a follow-up and drafts one.
      editableParams:
        - path: preset.seconds          # closed path list; cron expressions never
          label: Check every            # editable
          help: How often the agent looks for new deals.
          unit: minutes
          min: 60
          step: 60                      # whole minutes only

  paramsMeta:                           # display layer over the env contract
    CRM_DEFAULT_OWNER:                  # declared: --env-contract "CRM_DEFAULT_OWNER=…"
      displayName: Default deal owner
      description: Fallback owner when a deal has none.
      type: string
    TONE:
      displayName: Tone of voice
      type: enum
      enum: [formal, casual]
      default: casual
```

The publish-request-only sections for the same template — connection-event triggers, runtime dependencies, and outcomes:

```json theme={null}
{
  "declaredTriggers": [
    { "key": "deal-won", "connectionKey": "crm",
      "displayName": "When a deal is won",
      "description": "Drafts a thank-you follow-up the moment a deal closes.",
      "event": { "objectType": "deal", "event": "updated",
                 "filters": { "stage": "won" } },
      "instruction": "A deal was won — send a thank-you follow-up." }
  ],
  "channels": [
    { "channel": "whatsapp", "required": false,
      "description": "Follow-ups can go out on WhatsApp once a number is connected." }
  ],
  "outcomes": {
    "units": [
      { "key": "followup_sent", "title": "Follow-ups sent",
        "description": "A drafted follow-up approved and delivered to the prospect.",
        "unitLabel": "follow-up",
        "match": { "tools": ["crm_update_deal"], "connectionKey": "crm" },
        "events": [ { "event": "message.sent", "channel": "whatsapp" } ],
        "dedupe": "thread", "window": "24h",
        "reversals": [ { "onDeliveryFailure": true } ] },
      { "key": "deal_advanced", "title": "Deals advanced",
        "description": "A deal moved forward a stage after the agent's follow-up.",
        "match": { "tools": ["crm_update_deal"], "connectionKey": "crm" },
        "dedupe": "entity", "entityPath": "result.dealId", "window": "7d" }
    ]
  }
}
```

And the served manifest an installer's deploy screen reads (excerpt):

```json theme={null}
{
  "id": "9f2c1a8e-…", "name": "sales-followup",
  "displayName": "Sales Follow-up Agent",
  "description": "Chases deals with drafted follow-ups.",
  "version": 3, "latestVersion": 3, "visibility": "public",
  "contentHash": "sha256:…",                       // echo on install: the hash-seal
  "agent": { "model": "anthropic/claude-sonnet-4" },

  "skills": [
    { "key": "crm-sync", "displayName": "CRM sync",
      "description": "Reads deals and writes follow-up notes.", "version": "1.4.0" },
    { "key": "scheduler", "displayName": "Meeting scheduler",
      "description": "Books follow-up calls on your calendar.", "version": "2.0.1",
      "source": "marketplace",
      "marketplaceSkillId": "mskill_abc", "versionId": "mver_123" }
  ],

  "connections": [
    { "key": "crm", "capability": "crm", "required": true,
      "displayName": "Your CRM",
      "description": "Used to read deals and write follow-up notes.",
      "platforms": [                                // enriched from the catalog at read time
        { "type": "salesforce", "name": "Salesforce", "authSupport": "oauth" },
        { "type": "hubspot", "name": "HubSpot", "authSupport": "both" } ],
      "boundSkills": ["crm-sync"] },
    { "key": "notify", "capability": "chat", "required": false,
      "displayName": "Notifications channel",
      "description": "Optional: where follow-up alerts get posted.",
      "platforms": [ { "type": "slack", "name": "Slack", "authSupport": "oauth" } ] }
  ],

  "triggers": [
    { "key": "poll-crm", "type": "schedule",
      "displayName": "Check for new deals",
      "description": "Looks for deals needing a follow-up and drafts one.",
      "enabled": true,                              // recommended default, confirmed at deploy
      "preset": { "type": "interval", "seconds": 300 },
      "editableParams": [
        { "path": "preset.seconds", "label": "Check every",
          "help": "How often the agent looks for new deals.",
          "unit": "minutes", "min": 60, "step": 60 } ],
      "nextRuns": [ { "at": "2026-08-24T14:05:00Z",  // computed at read time
                      "timezone": "Europe/Copenhagen" } ] },
    { "key": "deal-won", "type": "connection-event", "connectionKey": "crm",
      "displayName": "When a deal is won",
      "description": "Drafts a thank-you follow-up the moment a deal closes.",
      "enabled": false }                            // event + instruction never serve
  ],

  "params": [
    { "name": "CRM_DEFAULT_OWNER", "displayName": "Default deal owner",
      "description": "Fallback owner when a deal has none.",
      "required": true, "type": "string" },
    { "name": "TONE", "displayName": "Tone of voice", "required": false,
      "type": "enum", "enum": ["formal", "casual"], "default": "casual" }
  ],

  "personaTemplate": {
    "template": "You are the sales assistant for {{COMPANY_NAME}}. …",
    "vars": [ { "name": "COMPANY_NAME", "displayName": "Your company",
                "description": "Used in the agent's replies and signatures.",
                "type": "string", "required": true, "placeholder": "Acme Inc.",
                "maxLength": 256 } ],
    "editable": true
  },

  "channels": [
    { "channel": "whatsapp", "required": false,
      "description": "Follow-ups can go out on WhatsApp once a number is connected." }
  ],

  "latestApprovedVersion": 3,
  "installable": true,
  "reconciliation": { "summary": { "satisfied": 1, "total": 2 },
                      "capabilities": { "…": "see the reconciliation block above" } }
}
```

***

## What the lints check

Publish is the quality gate: every violation is collected and returned in **one** response with field-path messages, and a rejected publish burns no version number. What you'll see, by field:

| Field                                                                             | Rejection you'll see                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| any frozen string field (skill code, job prompts, descriptions, env descriptions) | `SECRET_DETECTED: <pattern-name> in <kind>/<key>` — a hardcoded credential pattern; the location is named, never the value. Declare it in the env contract instead.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| any frozen code pointer                                                           | `CODE_BLOB_MISSING: <kind>/<key> references <hash>` — frozen code that could not be hydrated at apply time.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `jobs[].schedule`                                                                 | `SCHEDULE_INVALID:` — a `once` schedule (a frozen instant is stale by construction); interval seconds not a whole number of minutes ≥ 60; a cron that doesn't compile (5 fields required).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `triggerPresets.<key>`                                                            | `TRIGGER_PRESET_INVALID:` — no such trigger in this version; `enabled: true` on a connection-event key; `editableParams` on a connection-event key.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `triggerPresets.<key>.editableParams[]`                                           | `TRIGGER_PRESET_INVALID:` — a `path` outside `preset.seconds` / `preset.timezone`; duplicate path; missing `label`; `preset.seconds` on a non-interval (or `preset.timezone` on a non-cron) schedule; `min`/`step` not multiples of 60; `max < min`; the frozen schedule violating its own declared envelope.                                                                                                                                                                                                                                                                                                                                                                                                       |
| `personaTemplate.template`                                                        | `PERSONA_TEMPLATE_INVALID:` — a `{{TOKEN}}` with no `vars[]` entry; fewer than 40 literal author characters beside the slots.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `personaTemplate.vars[]`                                                          | `PERSONA_TEMPLATE_INVALID:` — a var appearing in no branch; a duplicate var name; a reserved name (`persona`); an invalid name; missing `displayName`/`description`; a name colliding with a declared param; `enum` empty/duplicated or on a non-enum type; a `default` not matching its type; a non-required var without a `default`; `maxLength` outside 1–2000 or on a non-string.                                                                                                                                                                                                                                                                                                                               |
| `paramsMeta.<key>`                                                                | `PERSONA_TEMPLATE_INVALID:` — no matching `envContract` entry; missing `displayName`; the same typed-declaration self-consistency rules as vars.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `connections[]`                                                                   | `CONNECTION_DECLARATION_INVALID:` — empty/duplicate `key`; missing `displayName`/`description`; non-boolean `required`; invalid `ownerType`; empty `platforms` or duplicate platform types; a `boundSkills` entry naming no skill in the version; scope declarations while scope capture is disabled.                                                                                                                                                                                                                                                                                                                                                                                                               |
| `connections[].platforms` (multi-platform)                                        | the portability lint: `Skill '<key>' calls platform-native tools ('<match>') — narrow connections['<key>'].platforms to that platform or migrate the skill to useCapability('<key>')`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `declaredTriggers[]`                                                              | field-path errors — key clashes with the version's trigger key-space; missing `displayName`/`description`/`instruction`; `instruction` over 2000 characters or interpolating event-payload fields; `connectionKey` naming no declared connection; an event not deliverable on every declared platform, or a filter that platform doesn't offer.                                                                                                                                                                                                                                                                                                                                                                     |
| `channels[]` / `features[]`                                                       | field-path errors — unknown channel/feature name (the known vocabulary is listed in the message); duplicates; non-boolean `required`; `displayName` over 120 or `description` over 2000 characters.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `outcomes`                                                                        | field-path errors (`outcomes.units[2].window: …`; evidence-attribution failures carry `OUTCOMES_MANIFEST_INVALID:`) — `verification`/`samplingRate`/`judge` present (platform-owned); more than 25 units; missing/duplicate unit `key` or missing `title`; an invalid `dedupe` mode; `window` outside `1h`–`30d` or malformed; a unit with neither `match` nor `events`; empty or over-long (128+ char) tool globs; a `connectionKey` that isn't a declared connection; an `event` outside the recordable vocabulary; a missing/wrong `channel` on `message.sent`; duplicate event evidence; an `entityPath` not starting `input.`/`result.` (or missing for `dedupe: entity`); a reversal with no evidence source. |
| source agent with voices or device triggers                                       | `UNSUPPORTED_SOURCE:` — templates don't freeze voice or device surfaces in V1; the error names each surface and says to remove it from the source agent before publishing.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |

## Related

* [Agent Templates](/marketplace/agent-templates) — the model: what a template contains, lifecycle, consent
* [Publishing Templates](/marketplace/publishing-templates) — the authoring journey this reference backs
* [Deploying Templates](/marketplace/deploying-templates) — how installers experience each section
* [Marketplace Command reference](/cli/marketplace-command)
