Build a template

Build your own Shardflux template from template.yaml: languages, packages, files, build steps, start commands and services, with the CLI, SDKs or MCP.

A template is a recipe

A template you build is described by one document, template.yaml (recipe v2):

  • base: the template it starts from, as <slug>@<version>, such as ubuntu-24.04@1.
  • build: what the build adds: languages, apt, pip and npm packages, files from your machine, and named build steps.
  • settings: what every workspace of the template gets when it opens: environment, inputs, start commands, services and defaults. See Templates.

Each build creates a new, numbered version of an organization template. You can build the same file with the CLI, the TypeScript SDK, the Python SDK or the MCP server; they upload the same bytes and produce the same recipe hash.

Write template.yaml

The CLI writes a starter file for a base:

Shell
shard templates init --base ubuntu-24.04

A fuller example. Local from paths are relative to the file: a folder is uploaded as a tar, a file as it is.

YAML
# acme/template.yaml
base: ubuntu-24.04@1
build:
  languages: [{ id: python }, { id: node }]
  packages:
    apt: [jq]
    pip: { requirements: [/home/user/app/requirements.txt] }
  files:
    - { from: ./app, to: /home/user/app, owner: user }
    - { from: ./config/settings.toml, to: /home/user/.config/acme/settings.toml, owner: user, mode: "0600" }
  steps:
    - { name: install, run: npm ci, user: user, cwd: /home/user/app }
settings:
  env: { APP_ENV: development }
  inputs:
    PROJECT_NAME: { kind: text, required: true }
    OPENAI_API_KEY: { kind: secret, required: false }
  start:
    - { name: seed, when: create, run: python seed.py, user: user, cwd: /home/user/app }
  services:
    web: { run: npm start, user: user, cwd: /home/user/app, ready: { port: 3000 } }

Quote file modes ("0600"): YAML reads 0600 as a number.

Build it with the CLI

Shell
shard templates languages --base ubuntu-24.04@1               # what build.languages offers on this base
shard templates packages search apt ffmpeg --base ubuntu-24.04@1
shard templates build acme/template.yaml --slug acme-dev --wait --timing

shard packs and uploads the local files (skipping bytes your organization already has), creates the build and, with --wait, follows it until the version is registered or the build fails. Progress goes to stderr. Without --slug, the template slug is the name of the directory holding the file; the first build creates the template.

The CLI leaves the new version unpublished unless you pass --publish, so you can try it first:

Shell
shard templates test acme-dev@1 --instance-key acme/dev-test --input PROJECT_NAME=demo   # a disposable session of version 1
shard ws exec acme/dev-test -- curl -s localhost:3000
shard ws close acme/dev-test

When it works, publish: build again with --publish, or publish the version from the console. New keys opened with --template acme-dev then get it:

Shell
shard templates build acme/template.yaml --slug acme-dev --publish --wait
shard ws open acme/main --template acme-dev --input PROJECT_NAME=acme

Exit codes: 6 when the build failed or was canceled, 5 when --timeout (default 30 minutes) ran out while the build continues, 2 when a local file cannot be read or packed or the API refused the recipe.

Build it with the TypeScript SDK

Reading YAML needs the optional yaml package (npm install @shardflux/sdk yaml); a JSON file needs nothing.

TypeScript
import { Shardflux } from '@shardflux/sdk';

const cloud = new Shardflux({ apiKey: process.env.SHARDFLUX_API_KEY! });

const { build, uploads } = await cloud.templates.buildFromFile('acme/template.yaml', {
  templateSlug: 'acme-dev',
  autoPublish: false,   // register it unpublished, test it, publish it later (the API default publishes at once)
  wait: true,           // until registered or failed (up to 30 minutes); default: return the queued build
  onProgress: (e) => console.log(e.type, e.type === 'build' ? e.build.state : e.from),
});
console.log(build.state, build.target_version, build.provenance.recipe_sha256, uploads.length);

// Try the version with its inputs before publishing it:
const test = await cloud.templates.versionTestInstances.create('acme-dev', build.target_version!, { inputs: { PROJECT_NAME: 'demo' } });
console.log(test.startup);   // start commands and services: pending | running | ready | failed
await test.close();

buildFromFile runs in Node.js only. It throws TemplateFileError before sending anything when a local file cannot be read or packed, and TemplateUploadError when the storage refuses an upload. Pass root to refuse local paths outside a directory.

Build it with the Python SDK

Reading YAML needs PyYAML: pip install 'shardflux[yaml]'.

Python
from shardflux import Shardflux

sf = Shardflux()

result = sf.templates.build_from_file(
    "acme/template.yaml", template_slug="acme-dev", auto_publish=False, wait=True
)
print(result.build["state"], result.build["registration"]["state"])
print(result.build["provenance"]["recipe_sha256"])
for u in result.uploads:
    print(u["from"], u["sha256"], "uploaded" if u["uploaded"] else "already there")

with sf.templates.version_test_instances.create(
    "acme-dev", result.build["target_version"], inputs={"PROJECT_NAME": "demo"}
) as test:
    print(test.exec("curl -s localhost:3000").stdout)

Without wait=True the call returns the queued build. Local problems raise TemplateFileError before any request, and a wait that runs out (default 1,800 seconds) raises TemplateBuildTimeoutError; the build keeps going.

Build it from a coding agent

The MCP server's template_build tool takes a recipe (recipe) or a template file (file). Every local path must be inside the server's working directory. The version stays unpublished unless the call passes publish: true, and wait: true follows the build within the call's deadline. template_get and template_languages let the agent look up versions, settings and languages first.

What a build does

The build runs in a VM on top of the base, in a fixed order: languages, apt packages, files, pip packages (into /opt/venv, owned by user), npm packages (installed globally), then your steps in order. Files come before pip, so an uploaded requirements.txt can be installed.

Field Notes
build.languages python, node, go, rust, java, each with an optional version; without one, the base's default. Listing pip or npm packages adds Python or Node.
build.packages apt: [...], pip: { packages: [...], requirements: [absolute paths] }, npm: [...]
build.files from (local path) or upload, to (absolute), owner (default root), mode (files only, default 0644), kind (file or tar, detected from from)
build.steps name, run (a shell script), user (default root), cwd, env
build.network build: auto (default), none or allowlist with allow_hosts; extra_hosts adds hosts to auto
settings env, inputs, start, services, defaults: see Templates

Build network. With auto, the build can reach only the hosts its recipe needs: the base's apt snapshot for apt packages, pypi.org and files.pythonhosted.org for pip, registry.npmjs.org for npm, the download hosts of the languages it installs, and your extra_hosts. If a step needs another host, the build lists it under denied_hosts; add it to build.network.extra_hosts and build again.

Services need a base whose guest agent supports them; otherwise the build is refused with services_unsupported.

Limits

Limit Value
files entries 1,000
steps 64
All scripts together (run, readiness commands, steps) 256 KiB
One upload 5 GiB
Entries in an uploaded folder 200,000
Size of what a version adds to its base The plan's per-workspace disk maximum: Free 2 GiB, Developer 20 GiB, Startup 50 GiB, Scale 100 GiB
Builds running at once per organization 3, unless the plan sets another value (403 quota_exceeded)

Refused before anything is sent: a from that is also an upload, a folder with kind: file, a compressed archive, sockets, FIFOs or devices in a folder, and symlinks that are absolute or leave the folder. The API refuses the rest with 422 validation_failed and a details.reason, such as language_unavailable, platform_owned_path (to under /proc, /sys, /dev, /run and a few files Shardflux owns) or invalid_settings.

When a build fails

  • shard templates builds get <build id> shows the state, the registration, denied hosts and the failure; shard templates builds log <build id> prints the full log.
  • Uploaded files are scanned for credentials. A finding fails the build; if a path is meant to be there, pass it with --acknowledge <path> (acknowledgedScanFindings, acknowledged_scan_findings), up to 200 paths.
  • A start command or service that fails does not fail the build. It fails the open of a workspace (startup_failed), which is why shard templates test is worth running before you publish.

Change an existing template

Export the template.yaml a version was built from, edit it and build it again. The same inputs give the same recipe_sha256:

Shell
shard templates export acme-dev@3 --out acme/template.yaml
TypeScript
const exported = await cloud.templates.versions.recipe('acme-dev', 3);

Existing workspaces keep the version they were created from; new keys get the published version.

Other ways to make a template

  • Save a workspace as a template: set up a workspace by hand or with an agent, then shard ws save-as-template <key> --template acme-dev --wait (or workspace.saveAsTemplate(...), ws.save_as_template(...)). The workspace's filesystem becomes the new version, except /tmp, /proc, /sys, /dev, /run, shared volumes and a list of platform files.
  • Drafts: shard templates draft open acme-dev --base python-node-browser@<version> opens a workspace you edit live; capture states, test them in disposable copies, then shard templates draft publish acme-dev.

Both need a layered workspace. Building, saving and drafts are open to organization owners and admins and to API keys with a tool permission.

View this page as Markdown