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

# Publishing Templates

> Turn a working agent into a template: draft, author, publish, review, and roll out updates

## From agent to template

You never hand-write a template manifest. You build a real agent, and the platform does the heavy lifting: publishing **freezes every primitive** from your promoted agent version automatically, and a **draft composer** infers the rest — connections, persona variables, trigger presets — into your project's `lua.skill.yaml` for you to review like a diff. Your job is the product decisions: which connections are required, what the installer-facing copy says, which automations should be on by default.

This guide follows one agent end to end: **Standup Sidekick**, a small agent that logs the wins a user mentions during the day (`log_win` / `list_wins` tools in a `standup-skill`), nudges them before standup with a scheduled `standup-nudge` job, and has a persona ("You are the Standup Sidekick for Acme Demo Co…").

<Note>
  New to templates? Read [Agent Templates](/marketplace/agent-templates) first for the model — what a template contains, lifecycle, and consent. This page is the hands-on authoring journey.
</Note>

## Step 1 — Build and promote the source agent

Nothing template-specific yet: build Standup Sidekick the normal way.

```bash theme={null}
lua push          # upload skills, jobs, persona
lua version promote <n>
```

Use it until you're happy with it. The promoted version is what publishing will freeze.

## Step 2 — Create the template listing

Run this from the project connected to your source agent:

```bash theme={null}
lua marketplace template create \
  --name standup-sidekick \
  --display-name "Standup Sidekick" \
  --description "Captures your wins during the day and has them ready for standup."
```

Templates are **private by default** — only your organization can find and install them. Add `--visibility public` to head for the public catalog (which adds a [review step](#step-6-visibility-and-review), below).

## Step 3 — Run the draft composer

```bash theme={null}
lua marketplace template draft --template-id tpl_standup
```

The draft composer writes a `template:` section into your `lua.skill.yaml`, composed from what your agent actually is:

| Section                                 | How it's composed                                                                                                                                                                                                                                             |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Skills, webhooks, jobs, triggers, model | Frozen automatically from your promoted agent version at publish — the composer doesn't need to draft these at all                                                                                                                                            |
| `connections`                           | **Inferred** from how your skill code reaches integrations, your webhook event subscriptions, and the connections live on your agent — with sibling platforms proposed for the allow-list and `required` proposed where a skill depends on it unconditionally |
| `personaTemplate`                       | **Detected**: `{{VAR}}` tokens in your persona are extracted into declared variables, with drafted display names and descriptions; hardcoded specifics (a company name) may be proposed for promotion into variables                                          |
| `triggerPresets`                        | **Derived** from your jobs: the schedule verbatim, `enabled` matching each job's current state on your agent, editable-parameter envelopes pre-filled with valid bounds                                                                                       |
| `paramsMeta`                            | Lifted from your env-contract metadata, with types and defaults proposed from examples                                                                                                                                                                        |

Re-running `draft` later (say, after adding a skill for v2) performs an **additive merge**: newly inferred entries are appended, your existing keys are never modified, and the CLI prints a per-section diff of what it added versus kept. A full overwrite of your authored sections requires an explicit `--force`. Use `--source-version <n>` to compose from a specific promoted agent version instead of the active one.

<Tip>
  Inference proposes; you dispose. Required-versus-optional, the platform allow-list, and every installer-facing string are product decisions the composer can only draft. Review the section like any diff before publishing.
</Tip>

## Step 4 — Edit the `template:` section

Here's a realistic authored section for Standup Sidekick after the composer ran and the creator polished it. The persona got two variables, the nudge job's interval became installer-editable, and an optional chat connection was declared for posting nudges:

```yaml theme={null}
template:
  personaTemplate:
    template: |
      You are the Standup Sidekick for {{COMPANY_NAME}}.

      You quietly capture wins your user mentions during the day and keep
      them ready for standup. Tone: {{TONE}}, zero fluff. Never invent wins.
    editable: true            # installers may tweak the resolved prompt at deploy
    vars:
      - name: COMPANY_NAME
        displayName: Your company
        description: Used in the agent's replies and signatures.
        type: string
        required: true
        placeholder: Acme Inc.
      - name: TONE
        displayName: Tone of voice
        description: How the agent phrases its nudges and summaries.
        type: enum
        enum: [brief and encouraging, strictly factual]
        default: brief and encouraging

  connections:
    - key: notify
      capability: chat
      required: false          # offered at deploy, never blocking
      displayName: Notifications channel
      description: "Optional: where standup nudges get posted."
      platforms:
        - type: slack
          name: Slack

  triggerPresets:
    standup-nudge:
      enabled: true            # recommended default — confirmed at deploy, never silent
      editableParams:
        - path: preset.seconds
          label: Nudge every
          help: How often the agent checks for unsummarized wins.
          unit: minutes
          min: 60
          step: 60             # whole minutes only

  paramsMeta:
    STANDUP_HOUR:
      displayName: Standup hour
      description: Local hour of your team's standup, 0–23.
      type: number
      default: 9
```

A few rules worth knowing while you edit:

* **Every installer-facing entry needs `displayName` and `description`.** The deploy screen renders its copy straight from this section — a raw key is never shown as a form label, and publish rejects entries missing them.
* **Keys are identity.** A connection `key`, trigger key, param name, or persona var name is its stable identity across versions — renaming one means "remove + add", which costs installers their setting for it. Keep keys stable across versions.
* **The whole section is serialized on every publish.** When a `template:` section exists, all five subsections (`connections`, `personaTemplate`, `triggerPresets`, `paramsMeta`, `onInstall`) are always sent — an empty or deleted subsection is an explicit *clear*, not "keep the previous version's". Clearing or narrowing anything prompts you with the consequence before publishing (see below).
* **The env contract is declared at publish**, via the `--env-contract` flag — `paramsMeta` carries the display metadata for those variables:

```bash theme={null}
--env-contract "STANDUP_HOUR?=Local hour of the team standup"
```

Every attribute you can author here — with types, defaults, constraints, and the exact lint each one is guarded by — is in the [Template Manifest Reference](/marketplace/template-manifest).

### The post-install first run (`onInstall`)

A template can declare what its agent should do **immediately after being installed** — before the installer has said a word to it:

```yaml theme={null}
template:
  onInstall:
    instruction: >-
      Introduce yourself, then confirm the notifications connection by
      posting a hello into the configured channel.
```

The platform delivers this as the agent's first message, one background turn fired right after a successful deploy or install:

```
[Install: Standup Sidekick] Introduce yourself, then confirm the …
```

Use it for the things every fresh install needs: a greeting that teaches the installer what the agent can do, a self-check of the connections it was given, or — the strongest use — running a setup tool (next section). The rules:

* **It's consent-surface copy.** The instruction is served verbatim on the manifest and shown on the deploy screen as *"After install, the agent will: …"* — the installer agrees to it before it runs. Write it for them, and keep it plain text: 1–2000 characters, no `{{var}}` tokens (nothing is substituted).
* **At most once per distinct instruction.** Repeat installs and updates that keep the instruction unchanged never re-fire it; changing the instruction in a new version fires it once more on update.
* **Fail-soft, background, as the installer.** The install never waits on the turn and never fails because of it, and the turn runs with the installing user's identity — an [Inbox push](/api/inbox) inside it reaches the installer's own desk.

### Self-wiring webhooks

The best template installs ask for **nothing**: no URL pasting, no provider settings screens. Two shipped pieces make that possible for webhook-driven templates:

1. **The platform injects each install's trigger URLs into the agent's env.** Every webhook trigger in your template gets its per-install URL written as `LUA_TRIGGER_URL__<TRIGGER_KEY>` (the trigger key upper-snaked — `github-pr-assigned` becomes `LUA_TRIGGER_URL__GITHUB_PR_ASSIGNED`), readable with [`env()`](/api/environment) from tools and trigger slots, kept fresh across token rotation, and removed at uninstall. See [LuaTrigger — Agents know their own URL](/api/luatrigger#agents-know-their-own-url).
2. **`onInstall` runs your setup tool on install.** Give the template a tool that reads the injected URL and registers it with the provider, and an `onInstall` instruction that tells the agent to run it and report.

A PR-review template wires GitHub this way — a `setup_github_watch` tool that discovers every repo the connected GitHub account administers and creates `pull_request` webhooks pointing at the injected URL:

```typescript theme={null}
// tools/SetupGithubWatch.ts (sketch)
import { LuaTool, env } from 'lua-cli';
import { z } from 'zod';

const TRIGGER_URL_ENV = 'LUA_TRIGGER_URL__GITHUB_PR_ASSIGNED';

export class SetupGithubWatchTool implements LuaTool {
  name = 'setup_github_watch';
  description = 'Wire pull_request webhooks on every repo the connected account administers.';
  inputSchema = z.object({});

  async execute() {
    const triggerUrl = env(TRIGGER_URL_ENV)?.trim();
    if (!triggerUrl) {
      return { ok: false, message: `${TRIGGER_URL_ENV} is not set — was this agent installed from a template?` };
    }
    // Idempotent: an existing hook with the same URL counts as already wired.
    return wireWebhooks(triggerUrl);
  }
}
```

```yaml theme={null}
template:
  onInstall:
    instruction: >-
      Set up GitHub watching now: run setup_github_watch and tell the user
      what you are watching.
```

The installer's whole experience: connect GitHub, confirm, and the agent's first message reports which repos it is now watching. Make the setup tool **idempotent** (safe to re-run — the instruction may fire again when you change it in an update) and have it read the URL from the env at call time, never cache it (the platform rewrites the variable if the trigger's token is rotated).

## Step 5 — Publish

```bash theme={null}
lua marketplace template publish \
  --template-id tpl_standup \
  --changelog "v1: win capture, standup nudges, Slack notifications (optional)"
```

Publishing freezes your agent's active promoted version plus the authored `template:` section into an immutable, integer-numbered template version (v1, v2, v3 — not semver). Use `--source-version <n>` to freeze a specific promoted version instead of the active one.

If this publish **clears or narrows** previously authored sections (you removed a declared connection, dropped a persona var), the CLI prints the consequence per section and asks for confirmation. `--yes` auto-confirms *only* that consequence prompt — useful in CI — without skipping any other confirmation the way `--force` does.

### The lint battery

Publish is the quality gate: a version that would fail on an installer's agent — or render an unusable deploy screen — is rejected **at publish**, with actionable errors, and a rejected publish burns no version number. Among what it checks:

* **Schedules must actually be schedulable.** Interval schedules run on whole minutes only — a fractional-minute interval would silently never fire, so it's rejected up front. Publishing Standup Sidekick with `seconds: 90` fails with:

  ```
  ✖ Publish rejected

  triggerPresets.standup-nudge → preset.seconds: 90 is not a whole number
  of minutes. Interval schedules must be a multiple of 60 (minimum 60).
  Did you mean 60 or 120?
  ```

* **Persona variables must be declared, both ways.** Every `{{VAR}}` in the persona template needs a matching `vars` entry, and vice versa — across every persona branch, including voice text. Reserved names (like `persona`) are rejected.

* **Declarations must be self-consistent.** Enum lists non-empty, defaults satisfying their own type, every `editableParams.path` resolving into its trigger entry, optional vars carrying a default.

* **Connections must be real.** Every platform in an allow-list must exist in the integration catalog and match the declared capability; OAuth platforms must declare their scopes (that's what the installer consents to).

* **Portability must be authored, not hoped.** A capability may list multiple platforms only when the skills bound to it reach the connection through the capability, not through platform-specific calls — otherwise the allow-list is narrowed to the platform the code actually uses.

* **No secrets in frozen code.** A hardcoded API key in skill source is caught and rejected, pointing at the file and line, with the fix: declare it in the env contract so each installer supplies their own.

* **No voice yet.** A source agent with voices or device triggers can't publish — voice templates aren't supported in V1, and the error says so rather than shipping a template that deploys half-working.

## Step 6 — Visibility and review

Private templates are installable by your org the moment they're published — this is the fleet path, and it needs no review.

Going **public** adds platform review, and review is **per version**:

* Flipping a private template public enqueues its latest version for review; it lists publicly once approved. Older published versions stay org-only unless republished.
* Every version you publish while public enters review, and isn't installable by others — including through the update badge — until approved.
* **Your own self-test installs are exempt**: you can install a pending version onto your org's agents to verify it before anyone else can.
* A rejection leaves your prior approved versions listed. Versions are immutable — resubmitting means publishing a new version with the fix.

## Step 7 — Versioning and consent

Two kinds of change behave differently when you ship v-next, and it pays to know which you're making:

* **Code-only changes** — you rewrote a tool's implementation, fixed a bug, improved a prompt inside a skill — flow to opted-in installs (`allowCreatorUpdates`) under their standing consent. This is the everyday fleet-update path.
* **Consent-surface changes** — a widened OAuth scope, an added or changed connection, a changed trigger instruction — require **re-consent from every install**, opted-in included. Your push is rejected per target until the installer reviews and accepts the new surface themselves.

Installers also always get a server-computed **update preview** before accepting: your changelog, what's added/removed/changed, scope deltas, and which of their armed automations survive. Their persona edits and schedule tweaks are preserved across your update — see [what survives an update](/marketplace/deploying-templates#updates) for the installer's side.

## Step 8 — Roll out

```bash theme={null}
lua marketplace template publish \
  --template-id tpl_standup \
  --changelog "v2: weekly summary skill, nudge copy fixes"

# Canary against a couple of agents first
lua marketplace template apply \
  --template-id tpl_standup \
  --agents agent_team_a,agent_team_b \
  --force

# Then the consenting fleet
lua marketplace template apply --template-id tpl_standup --all-installed --force
```

There's no separate canary flag — a canary is the same `apply` aimed at a smaller target list. Check the per-target result table (and the agents themselves) before going wide. Pass `--no-wait` to get the run ID immediately and check on it later with `status`.

What a rollout will and won't do on each target:

* **Won't arm anything new.** Renamed or newly added triggers land disabled, awaiting the installer's confirmation. A trigger the installer paused stays paused.
* **Won't strand targets on a new required env key.** Targets missing it flip to a pending-input state with an installer prompt, rather than shipping a broken agent.
* **Won't overwrite installer customizations silently.** Installer-edited personas are skipped; installer-tuned schedule params are re-applied over your new preset; hand-edits to managed primitives surface an explicit confirm instead of a silent revert.
* **Fails loudly.** Any failed target marks the run `failed` and exits non-zero; successful targets are unaffected and a re-run is safe.

```bash theme={null}
lua marketplace template status --template-id tpl_standup   # who runs which version
```

## Step 9 — Retire

* **Deprecate a version** to block new installs of it — existing installs are unaffected, and the changelog carries the reason. You can't deprecate the only approved version of a still-listed public template; unlist first.
* **Unlist the template** to soft-retire it: no new installs, no more creator applies. Existing installs keep running with what they have, can still uninstall, and can still re-run their same installed version to rebind a connection.

There is no hard delete — unlist + deprecate is the complete retirement path, and it keeps the audit trail and install ledger intact for every agent still running your template.

## Related

* [Agent Templates](/marketplace/agent-templates) — the model: what a template contains, lifecycle, consent
* [Template Manifest Reference](/marketplace/template-manifest) — every attribute: fields, types, defaults, constraints, lints
* [Deploying Templates](/marketplace/deploying-templates) — what your installers experience
* [Marketplace Command reference](/cli/marketplace-command)
* [Publishing Skills](/marketplace/creator-guide) — the single-skill equivalent
