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.

Install once with npm i -g shardflux, then use the shard command. If npm reports EACCES or EPERM, follow the user-directory install and use "$HOME/.local/bin/shard".

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.
  • immutable (optional): directories, such as your tooling, that every workspace shows from the template's newest published version. See Update tooling in every workspace.

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 over MCP

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
immutable Absolute paths of directories the build creates, shown read-only in every workspace from the template's newest published version. Omitted: the current version's list. See Immutable paths

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: Developer 50 GiB, Pro 50 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. To reach existing workspaces, put what they should share in immutable paths.

Update tooling in every workspace

Declare the directories that hold your tooling as immutable, and every workspace of the template shows them from the newest published version. Publish a fix once; existing workspaces have it at their next start or resume, and keep their own files, packages and work.

YAML
# acme/template.yaml
base: ubuntu-24.04@2
immutable: [/opt/acme]
build:
  files:
    - { from: ./acme, to: /opt/acme }   # bin/acme, lib/ and AGENTS.md
  steps:
    - { name: link, run: ln -s /opt/acme/bin/acme /usr/local/bin/acme }
settings:
  env: { ACME_HOME: /opt/acme }
Shell
shard templates build acme/template.yaml --slug acme-dev --wait     # version 5, unpublished
shard templates test acme-dev@5 --instance-key acme/tools-test      # a test instance shows version 5's /opt/acme
shard ws exec acme/tools-test -- acme --version
shard ws close acme/tools-test
shard templates publish acme-dev@5                                  # workspaces move to it at their next start or resume
  • Give each tool a directory of its own, such as /opt/<name>, and create it in the build: with a files entry, as above, or in a step.
  • Point at it, not at a release. A symlink on the rest of the disk or a variable in settings.env names the path, so it always reaches the current release and never has to change.
  • Keep work out of it. The paths are read-only in workspaces. Data an agent writes, caches and per-workspace configuration belong in the workspace's own files, such as /home/user.
  • Declare them in your first version. A workspace follows the immutable paths its own version declares, so workspaces opened from then on all stay current. A template.yaml without immutable builds a version with the current list, and you can add paths at any time.
  • Roll back by archiving the newest version: shard templates archive acme-dev@5. Workspaces return to the previous release at their next start or resume.

shard templates publish and archive act as a signed-in owner or admin; the console can publish versions too.

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. Immutable paths come from the template's newest published version, so a save never rolls tooling back.
  • 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. In a draft the immutable paths are writable, and publishing the draft publishes what you put there.

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