# Architecture and data flow
This page explains how `ibek` turns hand-authored support YAML into a running
IOC instance: which files exist, which command produces each one, and how the
internal modules fit together. It is aimed at contributors and advanced users.
For concept-level orientation start with the [overview](overview.md); for the
exact commands, options and field tables see the
[CLI reference](../reference/cli.md),
[support YAML reference](../reference/support-yaml.md) and
[ioc YAML reference](../reference/ioc-yaml.md).
## The file and schema lifecycle
`ibek` is split across three lifecycle phases — building a support image,
building a generic IOC image, and booting an IOC instance — each of which
produces and consumes specific files. The diagram below replaces the older
hand-drawn architecture image and uses the current file names throughout.
```{mermaid}
flowchart TD
subgraph author["Support module authoring (under ibek-support)"]
sup["*.ibek.support.yaml
(one per support module)"]
end
subgraph supbuild["Support image build"]
gss["ibek support generate-schema"]
supschema["ibek.support.schema.json
(global schema: validates
every *.ibek.support.yaml)"]
gss --> supschema
supschema -. "editor validation" .-> sup
end
subgraph iocbuild["Generic-IOC image build"]
gis["ibek ioc generate-schema
(combines ALL support yaml)"]
entschema["<container>.ibek.ioc.schema.json
(generic-IOC entities schema;
published to the release under
the fixed name ibek.ioc.schema.json)"]
gis --> entschema
end
subgraph instance["IOC instance authoring + boot"]
iocyaml["*.ibek.ioc.yaml
(instance author writes this)"]
gen["ibek runtime generate / generate2
(at container boot)"]
stcmd["st.cmd
(EPICS startup script)"]
subst["ioc.subst
(+ generated .db / pvi)"]
iocyaml --> gen
gen --> stcmd
gen --> subst
end
sup --> gss
sup --> gis
entschema -. "editor validation" .-> iocyaml
entschema --> gen
```
Reading the diagram:
- A support-module author writes one `*.ibek.support.yaml` per module. Each one
is validated against the single global `ibek.support.schema.json`, produced by
[`ibek support generate-schema`](../reference/cli.md). That schema is derived
from the `Support` pydantic model, so it describes the *shape of a support
file*, not the entities a particular image offers.
- At generic-IOC build time, [`ibek ioc generate-schema`](../reference/cli.md)
reads all the support YAML baked into the image and emits the **entities
schema** — generated locally as `.ibek.ioc.schema.json`. This schema
enumerates the concrete entity types the image can instantiate, and is
published as a GitHub release artifact alongside the container image under the
fixed name `ibek.ioc.schema.json` (no container prefix).
- An instance author writes `*.ibek.ioc.yaml`, validated in their editor against
that entities schema.
- At container boot, [`ibek runtime generate`](../reference/cli.md) (or the
newer `generate2`, which accepts multiple instance files) renders the EPICS
startup script `st.cmd` **and** the database substitution file `ioc.subst`
(plus generated `.db` and PVI/OPI files). Both outputs are first-class
products of the same render pass.
```{note}
The retired names `ibek.defs.schema.json` and
`.ibek.entities.schema.json` no longer appear anywhere in the code
or the produced artifacts. If you see them in older material, read them as
`ibek.support.schema.json` and `.ibek.ioc.schema.json`
respectively.
```
## The internal module pipeline
Inside a single `ibek runtime generate` invocation the work flows through four
collaborating modules. The function `do_generate` in
`runtime_cmds/commands.py` is the orchestrator that wires them together.
```{mermaid}
flowchart LR
yaml["*.ibek.support.yaml"] --> ef
ioc["*.ibek.ioc.yaml"] --> if
subgraph ef_box["EntityFactory"]
ef["make_entity_models()
one dynamic Entity class
per EntityModel"]
end
subgraph if_box["IocFactory"]
if["make_ioc_model() unions the
Entity classes into the IOC model
(discriminated by 'type')"]
des["deserialize_ioc()
builds Entity instances"]
if --> des
end
ef --> if
subgraph render_box["gen_scripts + Render / RenderDb"]
r["create_boot_script (Render)"]
rdb["create_db_script (RenderDb)"]
end
des --> r --> stcmd["st.cmd"]
des --> rdb --> subst["ioc.subst"]
```
What each stage does:
- **`EntityFactory` builds the types.** `make_entity_models()` reads each
`*.ibek.support.yaml`, validates it with the `Support` model, then calls
`create_model()` to mint one *dynamic pydantic `Entity` subclass* per
`EntityModel`. The equivalence "one `EntityModel` instance == one `Entity`
class" is central: instantiating an `EntityModel` *is* creating a new entity
type. The builtin `ibek.*` types (`RepeatEntity`, `Wait4IPEntity`,
`Wait4USBEntity`) are added to the same registry.
- **`IocFactory` unions them and deserialises.** `make_ioc_model()` takes the
list of Entity classes and forms a discriminated `Union` (keyed on the `type`
literal of each class) to produce an `IOC` model whose `entities` field
accepts any of them. This same model, serialised with
`model_json_schema()`, is exactly the entities schema published at build
time. `deserialize_ioc()` then loads an `*.ibek.ioc.yaml` and validates it
against that model, yielding concrete `Entity` *instances*.
- **`Render` and `RenderDb` emit the outputs.** Back in `do_generate`,
`create_boot_script()` (using `Render`) walks the entity instances and emits
`st.cmd`, while `create_db_script()` (using `RenderDb`) emits `ioc.subst`.
A few cross-cutting details that explain *why* the modules look the way they
do:
- **Pydantic models define the schema.** The `Param` subclasses
(`ObjectParam`, `IdParam`, `EnumParam`, and the scalar params) plus
`EntityModel` are ordinary pydantic models. The JSON schemas at every phase
are just `model_json_schema()` output, so the schema can never drift from the
validation logic.
- **Rendering order is fixed by a model validator.** The `add_ibek_attributes`
`model_validator(mode="after")` in `ioc.py` performs Jinja rendering of an
entity's fields in the order `pre_defines` -> `parameters` -> `post_defines`,
and stashes the defines on the instance so they are available to the final
render pass.
- **`UTILS` performs two-phase Jinja.** `utils.py` holds the singleton `UTILS`
renderer. Phase one runs during deserialisation (the model validator above);
phase two runs while `Render`/`RenderDb` build the output files, with a shared
global context (`_global`, `ioc_name`, variables created via `set`/`get`).
The template context available to authors is documented in the
[Jinja context reference](../reference/jinja-context.md).
## CLI groups mapped to lifecycle phases
`ibek`'s top-level command groups (see `__main__.py`) line up with the
lifecycle phases above:
| CLI group | Lifecycle phase | Role |
|-----------|-----------------|------|
| `ibek support` | Support image build | Compile/install support modules and emit the global `ibek.support.schema.json`. |
| `ibek ioc` | Generic-IOC image build | Combine all support YAML into the published entities schema; `ibek ioc do-wait` also runs at instance **runtime** to block boot until hardware is reachable. |
| `ibek runtime` | IOC instance boot | Render `st.cmd`, `ioc.subst` and runtime assets from the instance YAML. |
| `ibek dev` | Developer containers | Helpers for working *inside* a generic-IOC development container. |
| `ibek pattern` | Services repository | Vendor runtime-support patterns into a services repo and build the instance schema (see below). |
The **Role** column paraphrases each group's purpose for this page; it is not
the literal Typer `--help` text. The full command and option listing is in the
[CLI reference](../reference/cli.md); it is generated directly from the Typer
app so it cannot drift from the code.
## The runtime-support vendoring layer
The two diagrams above describe the build-time flow that predates the
`ibek pattern` feature. Vendoring adds a per-instance layer on top, so a
services repository can pin and carry the runtime support it needs without
rebuilding the generic-IOC image.
- Each IOC instance keeps **vendored copies** of the support patterns it uses,
pinned by a lock file named `runtime-lock.yaml` (`RUNTIME_LOCK_NAME` in
`globals.py`). The lock records exactly which version of each pattern the
instance is using.
- [`ibek pattern schema`](../reference/cli.md) builds a **self-contained**
instance schema. It fetches the image's *published base entities schema*
(the release asset `ibek.ioc.schema.json`) and programmatically merges the
instance's vendored and local support entities into the base schema's
discriminated `oneOf` / `discriminator.mapping` / `$defs`. The result is
written as `ioc.schema.json` (`IOC_SCHEMA_NAME`) at the instance root, and
the `ioc.yaml` schema header is rewritten to point at it.
- At boot, `ibek runtime generate2` picks up both the image's own support
definitions and the vendored support found in the instance config folder, so
the instance can add entities that the base image never knew about.
This supersedes an earlier design that wired support into the services repo
using git submodules and symlinks. The move from submodules to vendored copies,
and the reasons for it, are recorded in
[ADR 0004](decisions/0004-vendor-runtime-support-over-submodules.md); the
related decision that the vendored pattern tag is the source of authority is
[ADR 0003](decisions/0003-vendored-pattern-tag-is-authority.md).
This page stays at the architecture level — the operational steps for
vendoring, checking and restoring patterns live in the
[vendoring how-to](../how-to/vendor-runtime-patterns.md).
## See also
- [Overview](overview.md) — concepts and terminology.
- [CLI reference](../reference/cli.md) — every command and option.
- [Support YAML reference](../reference/support-yaml.md) — fields of
`*.ibek.support.yaml` and the schema artifacts.
- [ioc YAML reference](../reference/ioc-yaml.md) — fields of
`*.ibek.ioc.yaml`.
- [Jinja context reference](../reference/jinja-context.md) — what templates can
reference.
- [Architecture decision records](decisions.md) — the ADR index.