---
title: "kendor.yaml reference"
description: "Every key kendor.yaml accepts, from runtime and services to routing, databases and sandbox, with types, defaults and the rules checked on save."
updated: 2026-10-06
canonical: https://kendor.io/docs/environments/kendor-yaml-reference
---

# kendor.yaml reference
`kendor.yaml` sits at the root of a challenge's project and tells the sandbox what to run: one
language and version, optional extra languages and databases, and one or more services with their
install, dev and test commands. Only `version`, `runtime` and `services` are required. Kendor checks
the whole file when you save, and a file with errors is not saved.

## Where the file lives and when it applies

The file is `kendor.yaml` at the workspace root. In an uploaded zip or imported repository, Kendor
also accepts the `kendor.yml` spelling.

In the challenge editor, open **Settings** and choose **Configure runtime…**. The **Runtime
configuration** dialog has a **Form** tab and a **Raw YAML** tab; saving writes `kendor.yaml` and
restarts the preview. If you edit the file by hand instead, a **kendor.yaml changed** banner appears,
because the sandbox still runs the previous setup until you choose **Save & restart**.

When you save, Kendor runs the checks listed under [Validation rules](#validation-rules) and also
checks that every language and database version is one Kendor offers. Each problem is reported with
its path in the file, for example `services.web.dev.port`.

The copy of `kendor.yaml` inside a running sandbox is regenerated from the saved configuration each
time the sandbox starts, so it always shows what the sandbox is running.

> **Tip.** Start the file with
> `# yaml-language-server: $schema=https://kendor.dev/config-schema/v1`. Editors that use the YAML
> language server then offer autocomplete and inline errors for every key on this page.

### What candidates can change

A candidate can edit `kendor.yaml` in their own workspace. Only four top-level keys are read from
their copy: `install`, `services`, `preview` and `routing`. Their changes apply when the sandbox
restarts, and only if the merged file still passes validation; otherwise your configuration stays in
place. `runtime`, `toolchains`, `databases`, `sandbox` and `kendor` always come from your file, and
so do test commands: any `test` a candidate writes is dropped, and your `test` commands are kept on
the services you defined.

## A complete example

A Node.js API and a Vite frontend, with PostgreSQL and Valkey, plus a Python worker:

```yaml file="kendor.yaml"
# yaml-language-server: $schema=https://kendor.dev/config-schema/v1
version: 1
runtime:
  type: node
  version: "22"
  packageManager: npm
toolchains:
  - type: python
    version: "3.12"
databases:
  - type: postgres
    version: "18"
    name: shop
    init: db/init
  - type: valkey
    version: "9"
services:
  api:
    workDir: api
    install:
      commands:
        - npm ci
    dev:
      command: node --watch src/server.js
      port: 3000
      env:
        vars:
          WEB_URL: routing.web.url
          CACHE_URL: databases.valkey.url
          LOG_LEVEL: lit:debug
    test:
      command: node --test
  web:
    workDir: web
    install:
      commands:
        - npm ci
    dev:
      command: npm run dev -- --port $PORT
      port: 5173
      env:
        vars:
          VITE_API_URL: routing.api.url
    publicEnvPrefix: VITE_
  worker:
    workDir: worker
    install:
      commands:
        - pip install --user -r requirements.txt
    dev:
      command: python worker.py
preview:
  service: web
routing:
  - path: /api
    service: api
  - path: /
    service: web
requests:
  - name: List products
    url: http://localhost:3000/api/products
  - name: Create a product
    method: POST
    url: http://localhost:3000/api/products
    headers:
      Content-Type: application/json
    body: '{"name": "Mug", "price": 12}'
kendor:
  gradeService: api
```

## Top-level keys

Unknown keys are rejected everywhere in the file except inside `kendor`.

| Key | Type | Required | Description |
|---|---|---|---|
| `version` | number | Yes | The file format. Must be `1`. |
| `runtime` | mapping | Yes | The main language and its version. |
| `toolchains` | list | No | Extra languages in the same sandbox. |
| `databases` | list | No | PostgreSQL and Valkey servers inside the sandbox. |
| `install` | mapping | No | Commands run once at the workspace root before any service. |
| `services` | mapping | Yes | The processes to install, start and test, keyed by name. At least one. |
| `preview` | mapping | When there are 2+ services | Which service the preview pane shows. |
| `routing` | list | No | Path prefixes on the preview address and the service each one goes to. |
| `sandbox` | mapping | No | Docker or Kubernetes inside the sandbox. |
| `requests` | list | No | Example HTTP requests for the editor's **Requests** tab. |
| `kendor` | mapping | No | Author-only settings. |

> **Note.** Write versions as quoted strings: `version: "3.12"`, not `version: 3.12`. Unquoted, YAML
> reads `3.12` as a number and the file fails validation. The top-level `version: 1` is the exception;
> it must be the number `1`.

## runtime

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `type` | string | Yes | | One of `node`, `php`, `python`, `go`, `java`, `rust`, `cpp`. |
| `version` | string | Yes | | A version Kendor offers for that language, such as `"22"` or `"1.27"`. See [Languages and versions](/docs/environments/languages-and-versions). |
| `packageManager` | string | No | Not set | `npm`, `pnpm`, `yarn` or `bun`. Recorded for reference; what actually installs is your `install` commands. |

A challenge's language is fixed when it's created, but its version isn't: when `runtime.type` is the
challenge's language, `runtime.version` decides which version the sandbox runs.

## toolchains

A list of extra languages, each with its compiler, tools and language server on the `PATH` beside
the main one. Use it for a Go service next to a Node app, or Node for asset builds in a PHP project.

| Key | Type | Required | Description |
|---|---|---|---|
| `type` | string | Yes | Same choices as `runtime.type`. |
| `version` | string | Yes | A version Kendor offers for that language. |

Each language may appear once across `runtime` and `toolchains`.

## databases

A list of database servers that run inside the sandbox on `127.0.0.1` and start before any install
command. See [Databases in the sandbox](/docs/environments/databases) for connection details.

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `type` | string | Yes | | `postgres` or `valkey`. Each type at most once. |
| `version` | string | Yes | | `"18"` for PostgreSQL, `"9"` for Valkey. |
| `name` | string | No | `app` | PostgreSQL only. The database to create: lowercase letters, digits and underscores, starting with a letter or underscore, at most 63 characters, and not `postgres`, `template0` or `template1`. |
| `init` | string | No | Not set | PostgreSQL only. A `.sql` file, or a folder whose `.sql` files run in name order, relative to the workspace root. Runs once, after the database is created. |

## install

Commands that run once at the workspace root, before any service starts. Use it for a single
install that covers the whole repository, such as `pnpm install` in a monorepo.

| Key | Type | Required | Description |
|---|---|---|---|
| `commands` | list of strings | Yes | At least one. They run in order; if one exits non-zero, the commands after it are skipped. |

Databases are already running when these commands start, so a migration step here can reach them.

## services

A mapping from service name to service. Names start with a lowercase letter and contain only
lowercase letters, digits and hyphens, for example `api` or `web-app`. The name `kendor-db` is
reserved. Each service runs in its own process window in the sandbox.

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `workDir` | string | No | `.` | Folder the service's install, dev and test commands run in, relative to the workspace root. No leading `/` and no `..`. |
| `install` | mapping | No | | `commands`: a list of at least one command, run in order in `workDir` after the top-level `install`. If one fails, the service's `dev` command doesn't start. |
| `dev` | mapping | No | | The long-running process. See below. |
| `test` | mapping | No | | `command`: the command the **Tests** tab runs. |
| `publicEnvPrefix` | string | No | | The prefix of variables that are safe to expose to browser code, such as `VITE_`. Accepted and stored; the sandbox doesn't act on it. |

Every service needs at least a `dev` or a `test`. A service with only `test` (and no `install`) gets
no process window: this is how single-file challenges are set up.

When a service restarts, its install is skipped if the dependency files in its `workDir`
(`package.json`, lockfiles, `composer.json`, `requirements.txt`, `pyproject.toml`, `go.mod` and
similar) haven't changed.

### dev

| Key | Type | Required | Description |
|---|---|---|---|
| `command` | string | Yes | The command that starts the service, for example `npm run dev -- --port $PORT`. |
| `port` | integer | No | 1 to 65535. The port the service listens on. Two services can't share a port. |
| `env.vars` | mapping | No | Environment variables for this command. Names match `[A-Za-z_][A-Za-z0-9_]*`; values are references, described below. |

Kendor sets these variables for every dev command, and they override anything in `env.vars`:

| Variable | Value |
|---|---|
| `HOST` | `0.0.0.0` |
| `PORT` | The service's `port`, when it has one. |
| `KENDOR_PUBLIC_HOST` | The sandbox's public preview host name. |
| `KENDOR_VITE_ORIGIN` | The service's public origin: the preview address for the preview service, or the service's own port address for any other service with a port. Read by the `@kendordev/vite` plugin. |

Bind your server to `$HOST` and `$PORT` (or `0.0.0.0` and the declared port) so the preview can
reach it.

### Environment variable references

A value in `env.vars` must use one of four forms. Anything else fails validation.

| Form | Example | Resolves to |
|---|---|---|
| `routing.<service>.url` | `routing.api.url` | That service's public URL: the preview address plus the service's longest `routing` path. The preview address alone when the service's path is `/` or it has no route. The service must exist. |
| `databases.<type>.url` | `databases.postgres.url` | The connection URL of a database declared under `databases`. |
| `vars.<NAME>` | `vars.sessionId` | A value Kendor provides: `assessmentId`, `challengeId` or `sessionId`. Other names fail validation. |
| `lit:<text>` | `lit:debug` | The text after `lit:`, as is. |

Databases also set their own variables, such as `DATABASE_URL` and `REDIS_URL`, for every process
in the sandbox. You only need `databases.<type>.url` when your code reads a different name.

## preview

| Key | Type | Required | Description |
|---|---|---|---|
| `service` | string | Yes, when `preview` is present | The service whose dev server the preview pane shows. It must exist. |

`preview` is required when there is more than one service. With one service, that service is the
preview. A project whose services have no `dev` command has no preview, only a terminal.

## routing

A list of routes on the sandbox's preview address. Each request goes to the route with the longest
matching path prefix. The full path is passed on, so a service routed at `/api` receives
`/api/products`, not `/products`.

| Key | Type | Required | Description |
|---|---|---|---|
| `path` | string | Yes | Starts with `/`. Add a `/` route as the catch-all. |
| `service` | string | Yes | An existing service. Routes to a service without a `dev.port` are skipped. |

With routes declared, the preview shows a short waiting page until every routed service is accepting
connections. See [Multi-service apps](/docs/environments/multi-service-apps).

## sandbox

Lets the sandbox run its own container tools.

| Key | Type | Default | Description |
|---|---|---|---|
| `containers` | boolean | `false` | A Docker daemon inside the sandbox, with `docker`, `docker compose` and `buildx`. |
| `kubernetes` | boolean | `false` | A single-node k3s cluster started at boot, with `kubectl` and `helm`. Turns on `containers` too. |

Either one gives the sandbox more memory and CPU. The preview can open ports published by
`docker run -p` or Compose, and `kubectl port-forward` with `--address 0.0.0.0`. A configuration with
`containers` or `kubernetes` can't define any `test` command, so there's nothing to run from the
**Tests** tab; use it for challenges you review by reading the work. The pricing page lists container
environments under the Enterprise plan.

```yaml file="kendor.yaml"
version: 1
runtime:
  type: node
  version: "22"
sandbox:
  containers: true
services:
  app:
    install:
      commands:
        - npm ci
    dev:
      command: docker compose up
```

## requests

Example HTTP requests that appear, ready to send, in the editor's **Requests** tab. Requests are sent
from inside the sandbox, so `localhost` works as it does with `curl` in the terminal. They never
change how the sandbox starts.

| Key | Type | Required | Default | Description |
|---|---|---|---|---|
| `name` | string | Yes | | The label shown in the list. |
| `method` | string | No | `GET` | `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD` or `OPTIONS`. |
| `url` | string | Yes | | A full URL such as `http://localhost:3000/api/todos`, or a path such as `/api/todos`, which is sent to the preview service's port. |
| `headers` | mapping | No | | Header name to value. |
| `body` | string | No | | The request body. |

An example with a mistake is left out of the tab; the others still show.

## kendor

Author-only settings. A candidate can't change them. The block accepts extra keys without error.

| Key | Type | Description |
|---|---|---|
| `gradeService` | string | The service whose `test.command` the **Tests** tab runs. Defaults to the preview service. Must name an existing service. |
| `gradeServices` | list of strings | Several services whose test commands the **Tests** tab runs, each in its own `workDir`. Results are listed as `service › test`. Every service named must exist and have a `test`. Use either this or `gradeService`, not both. |

The block also holds keys that Kendor's own starter catalog uses to describe its starters: `title`,
`entryFile`, `order`, `type`, `difficulty`, `category`, `tags`, `editablePaths` and `solutionDir`.
They have no effect on your challenge.

## Validation rules

A file is saved only when all of these hold:

- `version` is `1`, and `runtime` and at least one service are present.
- No unknown keys outside `kendor`.
- Service names match `^[a-z][a-z0-9-]*$` and none is `kendor-db`.
- Every service has a `dev` or a `test`.
- `workDir` and `databases[].init` are relative paths inside the workspace: no leading `/`, no
  drive letter, no `..`.
- No two services use the same `dev.port`.
- `preview.service` names an existing service, and `preview` is present when there are two or more
  services.
- Every `routing[].service` names an existing service, and every `path` starts with `/`.
- Every `env.vars` value uses one of the four reference forms; `routing.<service>.url` names an
  existing service, `databases.<type>.url` names a declared database, and `vars.<NAME>` is
  `assessmentId`, `challengeId` or `sessionId`.
- Each language appears once across `runtime` and `toolchains`, and each database type once.
- `name` and `init` appear only on a `postgres` database, and `name` follows the naming rule above.
- `kendor.gradeService` and every entry of `kendor.gradeServices` name existing services, entries
  of `gradeServices` have a `test`, and the two keys aren't used together.
- With `sandbox.containers` or `sandbox.kubernetes`, no service has a `test` and neither
  `gradeService` nor `gradeServices` is set.
- Every language and database version is one Kendor offers. Versions that are no longer offered for
  new challenges still pass, so a challenge that uses one keeps saving.
