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:- The authored sections — what you write. The
template:section of your project’slua.skill.yamlcarries the six authored sections (connections,personaTemplate,triggerPresets,paramsMeta,onInstall,onUninstall). 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, andreferenceEvalRunId(the eval run satisfying the publish gate, when an eval set exists). - 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 thecodeHash/consentSurfaceHashpair that powers update diffing and re-consent (creator-side version reads only — the two are never in the public manifest). - The served manifest — what installers read.
GET /marketplace/templates/:id/versions/:v/manifestserves 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-callerreconciliationblock, and the orgpolicyblock.
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 six 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. onInstall and onUninstall follow the same rule: on the publish API an omitted section inherits and an explicit null clears; from the CLI, deleting the section from your yaml clears it.
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 atlua marketplace template create, editable later (except name).
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.
agent
agent.personaTemplate
The system prompt as a template with{{VAR}} slots, each declared, typed, and explained. Authored in the yaml template: section.
Each entry of
vars[]:
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 avars[]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.
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:
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 yamltemplate: section. Each entry is a capability satisfiable by any platform in its allow-list.
Each entry of
platforms[]:
In the served manifest, each platform entry is enriched from the live integration catalog at read time — the frozen declaration stores only
{type, oauthScopes}:
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.
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.
Each entry of
editableParams[]:
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.
triggers[] merges the frozen schedules, your presets, and the declared connection-event triggers into one list:
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.
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: truein its preset is rejected. The installer must switch it on explicitly. eventandinstructionnever serve publicly. The served manifest lifts onlykey,connectionKey,displayName,description(as atriggers[]entry withtype: "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.
Lifecycle hooks: onInstall and onUninstall
The two optional lifecycle hooks are symmetric:onInstall runs right after a successful deploy or install-onto-agent; onUninstall runs at the start of an uninstall, before any teardown — while the template’s skills, connection bindings, and injected trigger URLs all still work. Both take the same shape, with at least one of the two fields:
onInstall, the removing user for onUninstall) with the same runtime context as a chat-turn tool call and a 180-second budget, and its outcome (success or failure, with a result summary) is recorded on the install. The instruction fires as one background agent turn, prefixed so the agent knows why it woke:
onInstall semantics
- Consumer-visible. Both fields are served on the manifest, so the deploy flow’s consent screen can show “After install, the agent will: …”. Write the instruction as copy an installer will read.
- At most once per distinct instruction. Each install runs a given instruction at most once: re-installing or updating to a version with the same instruction never fires a second turn; an update that changes the instruction fires again once.
- Fail-soft. The install/deploy never waits on the turn and never fails because of the hook; failures are recorded on the install for inspection.
- Runs as the installer. User-scoped work inside the hook (an Inbox push, a memory deposit) reaches the person who installed.
onUninstall semantics
- Runs before teardown. The hook executes while everything the cleanup needs still exists — the version’s skills, the bound connections, the injected
LUA_TRIGGER_URL__*variables. Use it to undo external state the install created: deregister provider webhooks, close out third-party resources, say goodbye. - Cleanup is best-effort and never blocks removal. The tool run is awaited (within its budget); the instruction turn is given a bounded grace and then teardown proceeds regardless — a slow turn can lose its access mid-flight. If cleanup must happen, put it in
tool, notinstruction. - The outcome is reported. The uninstall result carries the hook’s outcome (
{ ran, ok?, error? }), and it is recorded on the install before teardown proceeds — a failed cleanup is visible, never silent, but it never fails the uninstall. - Runs as the removing user. The tool executes with the identity of whoever is uninstalling (so their connections resolve); with no real acting user (internal removal paths), the tool is skipped and the turn falls back to a system identity.
- Consumer-visible. Served on the manifest so the uninstall confirmation can show “Before removal, the agent will: …”.
onInstall and its inverse wired to onUninstall — is walked through in Publishing Templates.
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):
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:
A
model value is not checked against the approved-model catalog at install. An unrecognised code falls back to the platform default at request time rather than failing the install, so an org exclusion added after install can never break a template update. The 256-character cap and the control-character check still apply.
The served params[] merges both, one entry per contract key:
The reserved LUA_TRIGGER_URL__ prefix
Environment keys starting with LUA_TRIGGER_URL__ are platform-owned and cannot appear in an env contract — publish rejects them with ENV_CONTRACT_RESERVED_PREFIX. At install, the platform writes one LUA_TRIGGER_URL__<TRIGGER_KEY> variable per webhook trigger in the template (the trigger key upper-snaked), holding that install’s own trigger URL. The keys are tracked as install-introduced — refreshed if the trigger’s token is rotated, overwriting any hand-set value of the same name, and removed at uninstall along with the rest of what the install introduced. See LuaTrigger — Agents know their own URL.
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:
features[] — platform feature flags the agent depends on (e.g. the RAG feature for a docs-answering template):
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 asverified) owned entirely by the platform: verification, samplingRate, and judge are rejected as creator fields.
outcomes.units[] (max 25 per version):
match — tool-call evidence:
events[] — platform-recorded event evidence (a closed vocabulary; anything else is rejected at publish):
reversals[] — each entry needs at least one evidence source:
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.Served-manifest-only fields
These exist only in the served manifest — derived or attached at read time, never authored:The reconciliation block
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 authoredtemplate: section of lua.skill.yaml for a Sales Follow-up agent — all four yaml sections:
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:Related
- Agent Templates — the model: what a template contains, lifecycle, consent
- Publishing Templates — the authoring journey this reference backs
- Deploying Templates — how installers experience each section
- Marketplace Command reference

