Templates
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 for how templates relate to snapshots and images.
Working with a template is three steps:
- Define the spec: a base image, a Git context, ordered build steps, runtime behavior, and default resources.
- 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.
- 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), 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); createTemplate and buildTemplate send it to the server.
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 });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)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))# 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-apiThe 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.
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 buildtemplates = 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 buildtemplates, 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 buildtenki 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-apiThe template spec
A template is a single typed document. You rarely write it by hand — the SDK builder in Build and use 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:
{
"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 (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 —
runAtisboot(the default when a runtime exists),build(started during the build so it can be snapshotted), ormanual. - When it is ready —
readyWhenlists port, localhost HTTP, and exec checks. They run together and all must pass once. The samereadyWhencontract 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
RUNNINGmeans the platform is ready: VM, guest agent, and networking. Application readiness has its own states —STARTING,READY,FAILED,STOPPED— and does not gateRUNNINGor affect provisioning latency and its metrics. Opt into waiting for it withwaitForRuntime.
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 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.
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.
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:
const images = await sandbox.listRegistryImages({ workspace: "myworkspace" });
const image = await sandbox.getRegistryImage("myworkspace/node-api");
const resolved = await sandbox.resolveRegistryRef("myworkspace/node-api:prod");images = client.registry.list("myworkspace")
image = client.registry.get("myworkspace/node-api")
resolved = client.registry.resolve("myworkspace/node-api:prod")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")tenki sandbox registry list --workspace myworkspace
tenki sandbox registry get myworkspace/node-api
tenki sandbox registry resolve myworkspace/node-api:prodNot 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 for the names in each language.