# 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](https://docs.shardflux.dev/concepts/templates.md#settings-environment-inputs-start-commands-and-services).

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:

```sh
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

```sh
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:

```sh
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:

```sh
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.

```ts
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](https://docs.shardflux.dev/guides/coding-agents.md)'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](https://docs.shardflux.dev/concepts/templates.md#settings-environment-inputs-start-commands-and-services) |

**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`:

```sh
shard templates export acme-dev@3 --out acme/template.yaml
```

```ts
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.
