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 asubuntu-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:
shard templates init --base ubuntu-24.04A fuller example. Local from paths are relative to the file: a folder is uploaded as a tar, a file as it is.
# 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
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 --timingshard 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:
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-testWhen it works, publish: build again with --publish, or publish the version from the console. New keys opened with
--template acme-dev then get it:
shard templates build acme/template.yaml --slug acme-dev --publish --wait
shard ws open acme/main --template acme-dev --input PROJECT_NAME=acmeExit 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.
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]'.
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 whyshard templates testis 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:
shard templates export acme-dev@3 --out acme/template.yamlconst 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(orworkspace.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, thenshard 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.