<!-- Source: https://docs.biosimulant.com/runtime/lab-manifest -->

# lab.yaml Schema

`lab.yaml` describes a composed simulation: model instances, wiring, runtime, and optional child labs.

Put a `README.md` beside each root or owned child `lab.yaml`. Biosimulant shows
it in the lab About view and falls back to generated details from `lab.yaml` when
it is missing. Keep README images under `assets/` and reference them with
relative paths such as `![Run results](assets/results.png)`, not base64.

## Schema

```yaml
schema_version: "2.0"
title: string
description: string

models:
  - path: string
    alias: string
    parameters: object

wiring:
  - from: string
    to:
      - string

runtime:
  duration: number
  communication_step: number
  settle_steps: number

io:
  inputs:
    - name: string
      maps_to: string
  outputs:
    - name: string
      maps_to: string

children:
  - path: string
    alias: string
    parameters: object
  - package: string
    version: string
    alias: string
    parameters: object
    # A matching biosimulant.lock entry is required.
```

## Runtime

`runtime.communication_step` is required and must be positive.
`runtime.settle_steps` is optional, must be a non-negative integer, and defaults to `0`.

```yaml
runtime:
  duration: 10.0
  communication_step: 0.1
  settle_steps: 0
```

Set input values at run time through the run modal or run payloads. Use
parameters for persistent scalar defaults that belong in the lab manifest.

Finite downstream report, export, or visualisation modules should implement
`execute()` with `ExecutionPolicy.ONCE_AFTER_RUN`. BioWorld drains arbitrary
chains of those modules automatically after the final simulation boundary.
`settle_steps` remains available for legacy temporal modules that deliberately
need zero-time propagation. Settling does not extend simulated time and does not
invoke canonical modules.

The old external tick field is not supported.

## Execution timing

Biosimulant sorts a lab into one of three timings using the
`biosim.execution_policy` each model declares in its `model.yaml` (see
[Execution policy](/runtime/model-manifest#execution-policy)). The timing
tells tools whether duration, communication step and settle steps matter for a
run.

| Timing | When | Run settings |
|---|---|---|
| `finite` | Every model declares a policy and none declares `each_window` | Studio, Desktop and `biosimulant labs serve` hide duration, communication step and settle steps when you run the lab |
| `temporal` | At least one model declares `each_window` | Shown |
| `unknown` | No model declares `each_window` and at least one model doesn't declare a policy | Shown |

Timing only changes which settings the Run dialog shows. The `runtime` block
keeps the same rules for every lab:

- `runtime.communication_step` is still required in `lab.yaml`, and
  `biosim.communication_step` is still required in each `model.yaml`.
- `runtime.duration` must stay positive. A zero-duration run only sets modules
  up, so run-once modules never run.

When no module runs each window, BioWorld crosses the whole run in one step
instead of stepping through empty communication windows. Before-run modules run
at the start and after-run modules run at the end, as before.

A lab can't override a model's policy. You can mix policies in one lab, but
data can only move forward through the phases:

| Source policy | Can feed |
|---|---|
| `once_before_run` | Any policy |
| `each_window` | `each_window` or `once_after_run` |
| `once_after_run` | `once_after_run` only |

Modules that run once can't form a cycle. Cycles between `each_window` modules
are still allowed.

When you validate a lab source directory, `biosimulant labs validate` prints the
lab's timing, names any models that don't declare a policy, and reports invalid
wiring between models that do. With `--json`, the timing is under
`metadata.execution`. A package-backed child lab counts as undeclared.

## Run input files

`biosimulant labs run --run-input-file inputs.json` overrides runtime values,
initial inputs and model parameters for one run without changing `lab.yaml`. The
file is a JSON object with optional `parameters` and `simulation_config` keys,
the same shape Studio and Desktop send:

```json
{
  "simulation_config": {
    "runtime": {
      "duration": 20.0,
      "initial_inputs": { "stimulus": 1.5 }
    }
  },
  "parameters": {
    "neuron": { "gain": 2.0 }
  }
}
```

- Runtime values (`duration`, `communication_step`, `settle_steps`) can go under
  `simulation_config.runtime` or directly under `simulation_config`. If both are
  set, `simulation_config.runtime` wins.
- Initial inputs can go under `simulation_config.runtime.initial_inputs`,
  `simulation_config.initial_inputs` or `parameters.initial_inputs`, merged in
  that order so a later location wins. Use a public lab input name or an
  `alias.port` reference as the key.
- Model parameters can be keyed by alias, as `parameters.<alias>` or
  `parameters.per_model.<alias>`. They're merged onto that model's `parameters`
  in `lab.yaml`.

## Models

Each `models[]` entry creates one module instance in the lab.

| Field | Meaning |
|-------|---------|
| `path` | Relative path to a local model directory |
| `alias` | Unique name used in wiring |
| `parameters` | Constructor overrides merged into `biosim.init_kwargs` |

Model entries use `path` only. Validation rejects `package` or `version` on a
model entry. To reuse a published Lab, add it under `children`.

Current-kernel labs should not declare per-entry timing or ordering fields. Promote model-specific configuration to `parameters`, and expose runtime values as typed `io.inputs`.

## Wiring

Connections use `alias.port` references:

```yaml
wiring:
  - from: retina.visual_stream
    to:
      - lgn.retina
  - from: lgn.thalamus
    to:
      - sc.vision
```

Fan-out is supported by listing multiple destinations under `to`.

## Composability

Expose lab-level ports with an `io` block:

```yaml
io:
  inputs:
    - name: stimulus
      maps_to: neuron.current
  outputs:
    - name: spike_train
      maps_to: recorder.spikes
```

Parent labs wire to `child_alias.external_port_name`.

## Nested child labs

```yaml
children:
  - path: ../labs/microcircuit
    alias: circuit_a
    parameters: {}
```

Children can be addressed in wiring just like models:

```yaml
wiring:
  - from: stim.current
    to:
      - circuit_a.stimulus
```

Package-backed child Labs use an exact `package` + `version` and require a
matching entry in the sibling `biosimulant.lock` with `artifact_sha256`. The
archive is verified and resolved into the parent Lab's
`.biosimulant/dependencies/` state. Path-backed children remain supported for
existing self-contained Labs. See [Compose Hub labs locally](/hub/compose-locally).

**Info:**

  For portable local package builds, prefer path-based children. Package-backed children always need an explicit version; they never fall back to `latest`.

## See also

- [How to write a lab.yaml](/runtime/write-lab-manifest)
- [model.yaml schema](/runtime/model-manifest)
