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’slua.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…”).
New to templates? Read Agent Templates first for the model — what a template contains, lifecycle, and consent. This page is the hands-on authoring journey.
Step 1 — Build and promote the source agent
Nothing template-specific yet: build Standup Sidekick the normal way.Step 2 — Create the template listing
Run this from the project connected to your source agent:--visibility public to head for the public catalog (which adds a review step, below).
Step 3 — Run the draft composer
template: section into your lua.skill.yaml, composed from what your agent actually is:
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.
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:
- Every installer-facing entry needs
displayNameanddescription. 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-contractflag —paramsMetacarries the display metadata for those variables:
Lifecycle hooks: onInstall and onUninstall
A template can declare what its agent does immediately after being installed — and what it does to clean up just before being removed:
tool and/or instruction (at least one), and they run tool first, then instruction:
toolnames a tool from the template’s own skills, executed directly with empty input — no agent turn, no model — as the installing (or removing) user, with a 180-second budget. Its outcome is recorded on the install. This is the deterministic half: wiring things up, tearing things down.instructionfires one background agent turn, delivered as the agent’s message with the lifecycle marker:[Install: Standup Sidekick] …or[Uninstall: Standup Sidekick] …. This is the conversational half: greeting, reporting, saying goodbye.
- Both hooks are consent-surface copy. They’re served verbatim on the manifest: the deploy screen shows “After install, the agent will: …” and the uninstall confirmation shows “Before removal, the agent will: …”. Write instructions for the installer to read — plain text, 1–2000 characters, no
{{var}}tokens (nothing is substituted). Atoolmust name a tool that exists in the version’s frozen skills; publish rejects a name that doesn’t resolve. onInstallfires 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.onUninstallruns before teardown, and never blocks it. Your cleanup gets the live agent — skills, bound connections, injected trigger URLs all still work. The tool run is awaited; the instruction turn gets a bounded grace before teardown proceeds regardless. Cleanup failure is recorded and reported in the uninstall result, but an uninstall never fails because cleanup did — designonUninstallas best-effort, and put anything that must happen in thetool.- Fail-soft, background, as the acting user. Installs never wait on (or fail because of)
onInstall; both hooks run with the acting user’s identity — an Inbox push inside one reaches the installer’s (or remover’s) own desk. Make both tools idempotent: a re-fired hook or a retried run must be safe.
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:- 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-assignedbecomesLUA_TRIGGER_URL__GITHUB_PR_ASSIGNED), readable withenv()from tools and trigger slots, kept fresh across token rotation, and removed at uninstall. See LuaTrigger — Agents know their own URL. onInstallruns your setup tool on install. Declare the tool in the hook so the platform executes it deterministically the moment the install completes, and pair it with an instruction so the agent reports the outcome.
setup_github_watch tool that discovers every repo the connected GitHub account administers and creates pull_request webhooks pointing at the injected URL:
Step 5 — Publish
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: 90fails with: -
Persona variables must be declared, both ways. Every
{{VAR}}in the persona template needs a matchingvarsentry, and vice versa — across every persona branch, including voice text. Reserved names (likepersona) are rejected. -
Declarations must be self-consistent. Enum lists non-empty, defaults satisfying their own type, every
editableParams.pathresolving 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.
Step 8 — Roll out
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
failedand exits non-zero; successful targets are unaffected and a re-run is safe.
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.
Related
- Agent Templates — the model: what a template contains, lifecycle, consent
- Template Manifest Reference — every attribute: fields, types, defaults, constraints, lints
- Deploying Templates — what your installers experience
- Marketplace Command reference
- Publishing Skills — the single-skill equivalent

