# Templates (https://tenki.cloud/docs/sandbox/templates)

> For the complete documentation index, see [llms.txt](https://tenki.cloud/llms.txt)

Define a typed template from a Git context, build it into a private digest-addressed image, and create sessions from it.

A template is a reusable definition of a prepared environment. Instead of fetching a repo and installing the same tools at the start of every session, you define the environment once as a typed recipe, build it, and create sessions from the resulting image, so every session starts in the same known state. See [Concepts](https://tenki.cloud/docs/sandbox/concepts.md#templates) for how templates relate to snapshots and images.

Working with a template is three steps:

1. **Define** the spec: a base image, a Git context, ordered build steps, runtime behavior, and default resources.
2. **Build** it. Tenki fetches the context, runs the steps, and captures the result. A successful build automatically registers a private image, versioned and addressed by a real content digest.
3. **Create** sessions from that image.

Publishing is a separate, optional step: it changes an image's visibility or sharing. Builds never make an image public on their own.

## Primitives

A template is defined by a `TemplateSpec`, the recipe you build. It is one of a few related things worth keeping straight:

| Primitive       | What it is                                                                                  | Lifetime                          |
| --------------- | ------------------------------------------------------------------------------------------- | --------------------------------- |
| `TemplateSpec`  | The recipe (see [The template spec](#the-template-spec)), written directly or via a builder | Local until you create a template |
| `Template`      | The saved recipe and its metadata. Not a session source                                     | Until deleted                     |
| `TemplateBuild` | One execution of a recipe, with logs, events, and provenance                                | Retained with the template        |
| `Image`         | The session source a build produces, addressed by a `sha256` digest                         | Until deleted from the registry   |

Deleting a template removes its recipe and build history but leaves images it already produced launchable until you delete them from the registry.

## Build and use a template

The SDKs give you a typed builder that compiles to the canonical spec document (see [The template spec](#the-template-spec)); `createTemplate` and `buildTemplate` send it to the server.

**TypeScript**
```typescript
import { TemplateSpec, TenkiSandbox } from "@tenkicloud/sandbox";

const sandbox = new TenkiSandbox();

// 1. Define the spec (compiles to the canonical JSON document)
const spec = new TemplateSpec()
  .fromImage("sandbox")
  .withGitContext({ repo: "https://github.com/acme/node-api", ref: "main" })
  .workdir("/home/tenki/app")
  .buildEnv({ NODE_ENV: "production" })
  .run("npm ci", { timeoutSeconds: 1800 })
  .start("npm run dev", {
    runAt: "build",
    snapshotMode: "filesystem",
    readyWhen: [{ http: { url: "http://127.0.0.1:3000/health", successStatusCodes: [200, 204] } }],
  })
  .resources({ cpuCores: 2, memoryMb: 4096, diskSizeGb: 10 });

// 2. Build it. Streams events; resolves to a private, digest-addressed image
const build = await sandbox.buildTemplate(await sandbox.createTemplate({ name: "node-api", spec }), {
  buildSecrets: { GITHUB_TOKEN: token },
  waitForCompletion: true,
});

// 3. Create a session from the built image
const session = await sandbox.create({ image: build.image, waitForRuntime: true });
```

**Python**
```python
from tenki import Client, TemplateSpec

client = Client()

spec = (
    TemplateSpec()
    .from_image("sandbox")
    .with_git_context(repo="https://github.com/acme/node-api", ref="main")
    .workdir("/home/tenki/app")
    .build_env({"NODE_ENV": "production"})
    .run("npm ci", timeout=1800)
    .start("npm run dev", run_at="build", snapshot_mode="filesystem",
           ready_when=[{"http": {"url": "http://127.0.0.1:3000/health", "success_status_codes": [200, 204]}}])
    .resources(cpu_cores=2, memory_mb=4096, disk_size_gb=10)
)

template = client.templates.create(name="node-api", spec=spec)
build = client.templates.build(template, build_secrets={"GITHUB_TOKEN": token}, wait_for_completion=True)
session = client.create(image=build.image, wait_for_runtime=True)
```

**Go**
```go
import (
    "context"
    "time"

    tenkisandbox "github.com/LuxorLabs/tenki-sdk-go/sandbox"
)

ctx := context.Background()
client, err := tenkisandbox.New()

secrets := map[string]string{"GITHUB_TOKEN": "<your-token>"}

spec := tenkisandbox.NewTemplateSpec().
    FromImage("sandbox").
    WithGitContext(tenkisandbox.GitContext{Repo: "https://github.com/acme/node-api", Ref: "main"}).
    Workdir("/home/tenki/app").
    BuildEnv(map[string]string{"NODE_ENV": "production"}).
    Run("npm ci", tenkisandbox.RunStepOptions{Timeout: 1800 * time.Second}).
    Start("npm run dev", tenkisandbox.StartOptions{RunAt: tenkisandbox.RunAtBuild}).
    SnapshotMode(tenkisandbox.SnapshotModeFilesystem).
    ReadyWhen(tenkisandbox.ReadyWhen{
        Checks: []tenkisandbox.ReadyCheck{tenkisandbox.ReadyHTTP("http://127.0.0.1:3000/health", 200, 204)},
    }).
    Resources(tenkisandbox.TemplateResources{CPUCores: 2, MemoryMB: 4096, DiskSizeGB: 10})

template, err := client.CreateTemplate(ctx, tenkisandbox.WithTemplateName("node-api"), tenkisandbox.WithTemplateSpec(spec))
build, err := client.BuildTemplate(ctx, template, tenkisandbox.WithBuildSecrets(secrets), tenkisandbox.WithWaitForCompletion(true))
session, err := client.Create(ctx, tenkisandbox.WithImage(build.Image), tenkisandbox.WithWaitForRuntime(true))
```

**CLI**
```bash
# 1. Scaffold and edit .tenki/template.json (the spec shown below)
tenki template init

# 2. Build it. Waits and streams events by default; prints the digest ref
tenki template build node-api

# 3. Create a session from the built image
tenki sandbox create --image node-api
```

The session comes up in the prepared state with no install step. `--no-wait` detaches the build; `Ctrl-C` stops local streaming only and never cancels the remote build.

## Manage templates

Templates and their images are scoped to the workspace bound to your API key, so template calls don't take a workspace ID. Updating a template's typed spec is an atomic full replacement. Deleting a template leaves images it already produced launchable — remove those from the registry separately.

**TypeScript**
```typescript
const templates = await sandbox.listTemplates();
const tpl = await sandbox.getTemplate("node-api");
await sandbox.updateTemplate(tpl, { spec }); // atomic replacement of the typed recipe
await sandbox.deleteTemplate("node-api");
await sandbox.cancelTemplateBuild(buildId); // stops a remote build
```

**Python**
```python
templates = client.templates.list()
tpl = client.templates.get("node-api")
client.templates.update(tpl, spec=spec)  # atomic replacement of the typed recipe
client.templates.delete("node-api")
client.templates.cancel_build(build_id)  # stops a remote build
```

**Go**
```go
templates, err := client.ListTemplates(ctx)
tpl, err := client.GetTemplate(ctx, "node-api")
tpl, err = client.UpdateTemplate(ctx, tpl, tenkisandbox.WithTemplateSpec(spec)) // atomic replacement
_, err = client.DeleteTemplate(ctx, "node-api")
_, err = client.CancelTemplateBuild(ctx, buildID) // stops a remote build
```

**CLI**
```bash
tenki sandbox template list
tenki sandbox template get <template-id>
tenki sandbox template delete <template-id>

# Cancel the active build; use --build N when several are running
tenki template build cancel node-api
```

## The template spec

A template is a single typed document. You rarely write it by hand — the SDK builder in [Build and use](#build-and-use-a-template) and the CLI's `template init` both compile to it, and it is the canonical form the server validates, fills in with defaults, and hashes. The builder is immutable — each method returns a new spec — and every SDK can import, export, and validate a spec locally (`fromJSON`, `toJSON`, `validate`); the server rejects unknown fields and unsupported spec versions. Here is the complete spec the quickstart above produces:

```json
{
  "specVersion": "tenki.template.v1",
  "base": { "image": "sandbox" },
  "workdir": "/home/tenki/app",
  "context": {
    "source": { "git": { "repo": "https://github.com/acme/node-api", "ref": "main" } },
    "checkout": { "dest": "/home/tenki/app", "mode": "contents" }
  },
  "build": {
    "env": { "NODE_ENV": "production" }
  },
  "steps": [{ "run": { "command": "npm ci", "timeoutSeconds": 1800 } }],
  "runtime": {
    "runAt": "build",
    "start": { "command": "npm run dev", "workdir": "/home/tenki/app" },
    "snapshotMode": "filesystem",
    "readyWhen": {
      "timeoutSeconds": 60,
      "pollIntervalSeconds": 1,
      "checks": [{ "http": { "url": "http://127.0.0.1:3000/health", "successStatusCodes": [200, 204] } }]
    }
  },
  "resources": { "cpuCores": 2, "memoryMb": 4096, "diskSizeGb": 10 }
}
```

Each block maps to one part of the recipe:

| Field         | Notes                                                                                                                       |
| ------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `specVersion` | Required. Currently `"tenki.template.v1"`; the server rejects unknown or unsupported versions.                              |
| `base`        | Exactly one of a Tenki [base image](https://tenki.cloud/docs/sandbox/base-image.md) (default `sandbox`), a parent template, or a parent snapshot. |
| `workdir`     | Default `/home/tenki/app`. Used for checkout, build commands, and runtime processes.                                        |
| `context`     | The build input. A Git repo and a ref (branch, tag, or SHA), a checkout destination and mode.                               |
| `build.env`   | Plaintext environment for build steps. Not supplied to Git checkout.                                                        |
| `steps`       | Ordered operations (see below).                                                                                             |
| `runtime`     | How the application runs and when a snapshot is captured (see below).                                                       |
| `resources`   | Default vCPU (`1`–`16`), memory (`512`–`65536` MB), and disk (`5`–`100` GB) for the build and for sessions.                 |

Steps run in order and support `run`, `copy` (from the Git context to an absolute guest path), `writeFile`, `mkdir`, `remove`, `rename`, `symlink`, and the package helpers `apt`, `pip`, `npm`, and `bun`. A `run` step takes a shell command or an argv list; shell commands run under `sh -lc`, with a default timeout of 1800 seconds.

## Snapshot modes

`runtime.snapshotMode` decides what a build captures.

|                  | `filesystem` (default)                                     | `memory`                                                             |
| ---------------- | ---------------------------------------------------------- | -------------------------------------------------------------------- |
| What is captured | The disk after setup. The runtime process is stopped first | The live disk **and** memory, with the runtime process still running |
| A new session    | Boots and starts the runtime fresh                         | Restores with the process already running, for a near-instant start  |
| Requirement      | Runtime stops cleanly within the grace period              | The process must stay alive through snapshot completion              |

A snapshot is captured only for a runtime started during the build (`runAt: "build"`). If a memory image cannot be restored on a given host, Tenki cold-boots the snapshot's own disk instead of a clean base image, starts the declared runtime, and waits for the same readiness before the session is ready.

## Runtime and readiness

Runtime is optional and declares exactly one of a single `start` command or a `processCompose` configuration. For multi-process apps, `processCompose` points at a config path with an optional workdir and explicitly listed env files; Tenki owns supervision, logging, restart, and shutdown.

* **When it starts** — `runAt` is `boot` (the default when a runtime exists), `build` (started during the build so it can be snapshotted), or `manual`.
* **When it is ready** — `readyWhen` lists port, localhost HTTP, and exec checks. They run together and all must pass once. The same `readyWhen` contract applies at build time, at boot, for manual starts, and for cold-boot recovery. A build snapshot is captured only after readiness.
* **Session readiness is separate** — a session reaching `RUNNING` means the platform is ready: VM, guest agent, and networking. Application readiness has its own states — `STARTING`, `READY`, `FAILED`, `STOPPED` — and does not gate `RUNNING` or affect provisioning latency and its metrics. Opt into waiting for it with `waitForRuntime`.

## Environment and secrets

Environment variables are plaintext configuration and appear in read APIs. Set build-scoped variables under `build.env` and runtime-scoped variables under `runtime.env`.

Build secrets are different: request-time values you pass to a build for private Git checkout or secret-dependent steps. They are encrypted, carried only as an opaque reference, and never reach the runtime or appear in logs, provenance, or snapshots. The CLI reads build secrets from explicit flags and can detect `GIT_TOKEN`, `GH_TOKEN`, or `GITHUB_TOKEN`. Redaction is best-effort — a command that deliberately prints a secret can still leak it, so keep secrets out of build output. Reusable, named secret references are not yet available; pass secrets per build.

## Image references

A build produces a private image in your workspace registry, referenced three ways:

* `acme/node-api` — the untagged reference, which tracks the newest successful build.
* `acme/node-api:prod` — an optional tag you manage, a moving reference.
* `acme/node-api@sha256:…` — an immutable digest that pins the exact built output.

The digest is computed over the image's launch artifacts and behavior, so identical inputs always produce the same digest. A failed or older build never moves the untagged reference. Older `name@<snapshot-id>` references still resolve but are deprecated; new builds emit digest references only.

## Build lifecycle

A build moves through `PENDING`, `BUILDING`, `READY`, and `FAILED`. Waiting for a build streams one ordered stream of log and progress events; a waited failure raises a typed error carrying the final, redacted build. Reconnect to an in-flight build to resume streaming. Editing a template does not affect a build already in flight — each build freezes its own spec on submission.

## Build from CI

Because the spec is a single file, templates fit a GitOps flow: commit it to your repository as `.tenki/template.json` and let CI rebuild the image whenever the spec changes. The [Tenki GitHub Actions](https://github.com/LuxorLabs/tenki-actions) wrap the CLI for this — `setup-cli` installs `tenki`, and `template-build` creates or updates the template from the committed spec, builds it, and outputs the immutable digest ref.

```yaml
name: Build sandbox template

on:
  push:
    branches: [main]
    paths: [".tenki/template.json"]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: LuxorLabs/tenki-actions/setup-cli@v1
      - uses: LuxorLabs/tenki-actions/template-build@v1
        id: template
        env:
          TENKI_API_KEY: ${{ secrets.TENKI_API_KEY }}
          GITHUB_TOKEN: ${{ github.token }}
        with:
          template: node-api
      - run: tenki sandbox create --image "${{ steps.template.outputs.image }}"
```

Scaffold the spec once with `tenki template init`, then commit it. On each run, `template-build` creates the template if the name is new or replaces its spec if it already exists, waits for the build, and sets `steps.<id>.outputs.image` to the digest ref for later jobs. The `TENKI_API_KEY` secret determines the workspace the template and image belong to.

Pass build secrets — such as a token for a private Git checkout — with the `build-secret-env` input, naming job env vars to forward for that build only. The well-known `GITHUB_TOKEN`, `GH_TOKEN`, and `GIT_TOKEN` are detected automatically. See [Environment and secrets](#environment-and-secrets).

## Publish and share

The registry is a per-workspace namespace of images. Each image has a visibility: `private` (the default), `public`, or `shared` with specific workspaces. Publishing changes visibility or grants sharing; it is always explicit. Share an image across workspaces with `tenki sandbox registry share --target-workspace <workspace-id>`.

Browse and resolve images:

**TypeScript**
```typescript
const images = await sandbox.listRegistryImages({ workspace: "myworkspace" });
const image = await sandbox.getRegistryImage("myworkspace/node-api");
const resolved = await sandbox.resolveRegistryRef("myworkspace/node-api:prod");
```

**Python**
```python
images = client.registry.list("myworkspace")
image = client.registry.get("myworkspace/node-api")
resolved = client.registry.resolve("myworkspace/node-api:prod")
```

**Go**
```go
images, err := client.ListRegistryImages(ctx, tenkisandbox.WithRegistryWorkspace("myworkspace"))
image, err := client.GetRegistryImage(ctx, "myworkspace/node-api")
resolved, err := client.ResolveRegistryRef(ctx, "myworkspace/node-api:prod")
```

**CLI**
```bash
tenki sandbox registry list --workspace myworkspace
tenki sandbox registry get myworkspace/node-api
tenki sandbox registry resolve myworkspace/node-api:prod
```

## Not supported

A few things are out of scope today:

* Local filesystem or archive build contexts — the build context is Git only.
* Launching a session from an inline recipe file — the template must exist as a resource first. Once it does, you can either build it into an image or launch a session directly from its stored spec with `tenki sandbox create --from-template-spec <name-or-id>`.
* Dockerfile ingestion and full devcontainer features.
* Per-build overrides for Git ref, environment, or resources.
* Automatic public or shared publishing, and turning build numbers into tags — visibility and tags are always explicit.

## Errors

Template and registry calls fail when a reference is unknown, a build fails, or a name collides. Every SDK surfaces these as typed errors or exceptions; see the [SDK reference](https://tenki.cloud/docs/sandbox/sdk.md#errors) for the names in each language.