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

# Lifecycle hooks

> onInstall and onUninstall fields, when each half runs, how a failed setup is retried, and the installPolicy section

Lifecycle hooks are the `onInstall` and `onUninstall` sections of the [template manifest](/marketplace/manifest): what the installed agent does right after an install and right before an uninstall. `installPolicy` limits how many installs one organization may hold. All three are authored by hand; `lua marketplace template draft` never composes them.

*Verified against lua-cli 3.33.0.*

## Shape

Both hooks take the same object with at least one field; any other field, or a section that is not an object, is refused. `{{var}}` tokens are refused in a hook; nothing is substituted.

<ParamField path="tool" type="string">
  Bare name of a tool in this version's frozen skills, executed directly with input `{}` and no model involved. A name that does not resolve against the frozen skills is refused at publish.
</ParamField>

<ParamField path="instruction" type="string">
  Plain text, 1 to 2000 characters after trimming, sent as one background agent turn. Served on the manifest so the installer reads it before consenting.
</ParamField>

<ParamField path="startWorkflow" type="{ workflow: string, input?: object }">
  `onInstall` only. Starts one run of a workflow frozen in this version, as the installer. `input` is at most 8 KB and is validated against the workflow's input schema.
</ParamField>

```yaml lua.skill.yaml theme={null}
template:
  onInstall:
    tool: setup_github_watch
    instruction: Report which repositories setup_github_watch wired, briefly.
  onUninstall:
    tool: unwire_github_watch
    instruction: Tell the user the GitHub webhooks were removed and say goodbye.
```

The recommended split: the tool does the work and the instruction is the conversation. Make the tool idempotent; a retry runs it again.

## onInstall timing

After a successful install or apply, on the first install and on every apply whose tool name or instruction differs from the last one that ran.

* The tool runs first, as the installer, with a 180-second budget. The instruction then fires as one background turn on channel `template-install`, prefixed `[Install: <displayName>] `; when the tool failed, the turn is told with `[Setup step "<tool>" failed: <error>]`.
* The install never waits on either half and never fails because of one. The response returns as soon as both are claimed; failures are recorded on the install.
* Idempotent per install: the tool is keyed by name and the turn by a hash of the instruction, so a re-run of the same version, a retry, or an update with an unchanged hook does not run it again. A changed tool name or instruction runs once more.
* A fleet `apply` or platform provisioning has no installer, so the hook runs in system scope. A one-call deploy defers both halves until the deployer's access to the deployed agent is registered; the install response then says `onInstallDeferred: true`.

## onUninstall timing

At the start of an uninstall, before any primitive, env key, subscription, or connection binding is removed, so the tool still has the skills, `LUA_TRIGGER_URL__*` values, and connections it needs.

* The tool is awaited: teardown waits for it, within its budget.
* The instruction fires on channel `template-uninstall`, prefixed `[Uninstall: <displayName>] `, and is awaited for at most 30 seconds; teardown then proceeds regardless. Cleanup that must happen belongs in `tool`.
* Both run as the removing user. With no acting user (an internal removal), the tool is skipped and the turn runs in system scope.
* The outcome `{ ran, ok?, error? }` is recorded on the install and returned as `onUninstall` on the uninstall response. Cleanup never fails the uninstall; the CLI prints `✅ Template uninstalled from this agent.` either way.

## Install result fields

`lua marketplace template install --json` prints these beside the fields on the [agent templates API](/reference/rest/agent-templates#install-result).

<ResponseField name="onInstallFired" type="boolean">
  `true` when this install claimed and fired the instruction turn; absent otherwise.
</ResponseField>

<ResponseField name="onInstall" type="{ toolRan: boolean, ok?: boolean }">
  `toolRan` is `true` when the tool was claimed. It runs in the background; read the result back as `setup`.
</ResponseField>

<ResponseField name="onInstallDeferred" type="boolean">
  `true` on a one-call deploy: the hooks fire after the deployer grant, so the outcomes come from that later call.
</ResponseField>

<ResponseField name="onInstallWorkflowRunId" type="string">
  The run `startWorkflow` started, or the run an earlier same-version install already started.
</ResponseField>

<ResponseField name="onInstallError" type="{ code, message }">
  Why `startWorkflow` started no run: `workflow-not-materialized`, `control-plane-unavailable`, `control-plane-dark`, `start-refused`, or `start-failed`. The install itself succeeded.
</ResponseField>

## Setup status and retry

`GET /agents/:agentId/templates` reports `setup` for each installed template that declares a tool or `startWorkflow`: `{ status: 'running' | 'ok' | 'failed', error?, code?, workflowRunId? }`. `failed` wins over `running`, which wins over `ok`. A tool marker with no result after about 220 seconds reports `Setup step did not finish`; a deferred hook whose fire never arrived reports `Setup step never started`.

`POST /agents/:agentId/templates/:templateId/setup/retry` re-runs the tool when its last run failed or died, re-attempts `startWorkflow`, and never re-fires the instruction turn. Its answer, whose `startWorkflow.outcome` is `started`, `already-started`, `failed`, or `not-declared`, is on the [agent templates API](/reference/rest/agent-templates). No CLI action calls either route.

## installPolicy

<ParamField path="perWorkspace" type="'single' | 'multiple'" required>
  `single` allows one installed row of the template per organization. `multiple`, the behavior when the section is absent, sets no limit.
</ParamField>

```yaml lua.skill.yaml theme={null}
template:
  installPolicy:
    perWorkspace: single
```

With `single`, a second install or deploy into an organization that already holds an `installed` row is refused with HTTP 409 `SINGLE_INSTALL_PER_WORKSPACE` before anything is created; the response names the agent that holds it. The holding agent may re-install or upgrade; an `apply_failed` row holds no slot; an uninstall frees it; an agent outside any organization is exempt. Installers see `This template can only be installed once per workspace, and this workspace already has it. Use the agent that has it, or uninstall it there first.`

`draft` drops the section, so re-add it after every draft. On the publish API an omitted `installPolicy` inherits the previous version's and `null` clears it; from the CLI, removing the section clears it and the consequence prompt prints `installPolicy: CLEARING the section — new installs are no longer limited to one per workspace`.

## Lints

| Code                                         | Raised when                                                                                  |
| -------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `ON_INSTALL_INVALID`, `ON_UNINSTALL_INVALID` | A hook outside the [Shape](#shape) rules; the message names the field and the reason         |
| `INSTALL_POLICY_INVALID`                     | `installPolicy` is not `{ perWorkspace: 'single' \| 'multiple' }`; the message names the key |

## See also

* [Template manifest](/marketplace/manifest) — the other sections and the served fields
* [Lints and consent](/marketplace/lints-and-consent) — every code and what installers consent to
* [Install and apply](/marketplace/install-and-apply) — where the hooks fire from
* [`LuaTool`](/reference/sdk/luatool) — writing the tool a hook names
* [`env`](/reference/sdk/env) — reading `LUA_TRIGGER_URL__*` inside a setup tool
