Format reference

Every file a package can carry, and every field in it, as the engine's reader parses it. Where a specification document describes a different shape, the shape below is the one that installs.

Conventionsnot documented marks a field that appears in real packages but whose consumer was not found in the source this reference was written from. An optional field has a defined default; an undocumented one has an unknown reader. Both are listed — only the first is safe to depend on.

manifest.yaml

Identity.

Required on every package, at the zip root. The engine's ReadPackage treats id as the only hard requirement — a manifest without one is an error and the package is not installed. Every other field parses to its zero value when absent.

Fields are grouped by who reads them.

Read by the engine

FieldReq.DefaultWhat it does
idyesThe package's permanent identifier. Lowercase, hyphens. Everything downstream — CLI, tool names, the index, install directories — keys off this and never off name.
nameno""The display label ("Whisper Tiny"). Changeable at any time; nothing internal depends on it.
versionno""The package's own semver, bumped on every republish. Distinct from naming.version.
typeno""model_pipeline | application. Normalised on read — legacy model_pipeline and model both mean pipeline. extension is reserved and appears only in engine test fixtures. Not validated against an enum: a typo passes silently.
runtimeno""venv | container. IsVenv() routes a venv package to the engine's own installer; anything else is routed elsewhere. No shipped pipeline declares it.
gpuno""required | optional | none. none — and absent — marks the model CPU-only, which exempts it from VRAM gating and from eviction.
min_vram_gbno0Minimum graphics memory in GB. Falls back to ui.hardware.min_vram_gb when the top-level field is 0, so a manifest declaring it only under ui: still classifies correctly.
recommended_vram_gbno0The comfortable amount the resource manager prefers for a full-GPU fit.
min_ram_gbno0Minimum system RAM, for CPU models. In the spec and in the struct; no shipped manifest declares it.
capabilitiesno[]What this package can do. Each entry is {id, modality}, both strings. The coarse list the engine and marketplace filter on — not the detailed contract, which is ui.capabilities.
descriptionno""One-line description for listings.
licenseno""A bare SPDX identifier (MIT, Apache-2.0). An object here yields "" — that shape belonged to older v2.0 packages, whose licence travelled inside the UI contract.
min_os_versionno""The lowest Hutash OS version this package supports. The engine relays it and never compares it. Absent means no floor, never unknown: a consumer must read empty as compatible. Install is never blocked on it; a consumer above the floor shows a warning and installs anyway. Quote it — unquoted YAML reads 1.0 as a float.
sourcenoabsent{repo, commit}. For application packages the field the installer actually uses lives in application/config/app.yaml and has a different shape.
uinoabsentThe host-integration contract. Carried by the engine as an opaque tree; it writes it back out as application/manifest.json at install and never interprets it.

Read by the index builder

Read by build_index.py when the catalogue is published.

FieldDefaultWhat it does
metadata.weight_categoryThe weights subfolder, and the index's coarse category for a pipeline (audio, LLM, …). Falls back to the first capability's modality, then to "pipeline".
metadata.category"app"The index's category for an application (photo, creative, …). Preferred over the top-level category, which is usually the literal string "app" and useless for grouping.
metadata.quality_scorenullPublished as quality_score, rendered as a 0–10 star score after backend rescaling.
metadata.speednullPublished verbatim as the index's speed.
metadata.descriptionFallback when the top-level description is empty.
metadata.internalfalseGates the package out of user-facing lists. prompt-engine uses it.
low_vram_capableomittedWhether an oversized model can be split across GPU and RAM. Emitted only when declared — for a CPU-only model the question is inapplicable, not false.
languageomittedThe model-level primary language, short code. Emitted only when declared. Distinct from the per-option language under ui:, which narrows a picker.
languagesomittedEvery language this build can actually handle. Emitted only when declared.

Present in shipped manifests, read by neither

Each of these appears in real packages. No reader for them was found — they are documentation, provenance, or consumed somewhere outside the source this reference was written from.

FieldConsumerWhat it appears to do
hutash_formatnot documentedFormat-generation stamp. Every shipped pipeline says '1.0'.
namingnot documentedThe structured metadata id is generated from — see below.
platformsnot documented[windows, linux] on every shipped pipeline.
categorynot documentedOn applications, usually the literal "app".
model_shelfnot documentedshared on both shipped applications — they read the one shared model store rather than keeping their own.
capabilities_neededVerticalsWhich engine capabilities the app requires (stt, llm, translate). The first-launch setup gate derives what it demands from this list, and the auto-generated Models page filters the marketplace to it.
metadata.homepagenot documentedProject URL for the store listing.
metadata.shelf_warningnot documentedPresent on hutash-studio.hutash as null. Purpose unknown.
permissionsReservedNot read by the engine or Studio, and absent from the engine's source entirely. If present it must be []; a non-empty list has no defined behaviour.

manifest.yaml

The naming: block.

Nested under naming: rather than top-level, because manifest.yaml already has its own version: — the package's semver — and these are different numbers.

FieldWhat it does
familyGroups variants of one model together (whisper, kokoro, gemma). Lowercase.
versionThe model maker's release number, not the package's. Unchanged when the package is merely rebuilt or re-quantized.
variantSize tier or special edition (tiny, edge).
parametersParameter count (82m, 1.7b, 4.5b).
quantizationCompression type (q4_K_M, q8_0).
formatRuntime format: gguf | pytorch | onnx | ctranslate2.

The rule the block describes: {family}[-{version}][-{variant}][-{parameters}][-{quantization}], joined by hyphens, skipping nulls, all lowercase, never a doubled, leading or trailing hyphen. architecture (dense/moe) and active_parameters (MoE only) are optional catalogue-display fields that describe the model rather than generating its id.

In practice an id is assigned once and never changes. whisper-tiny declares parameters: 39m and kokoro declares version: "1.0" and parameters: 82m, yet both ids are short — both predate the block. Read naming: as documentation of a package, not as the thing that derives its id.

manifest.yaml → ui:

The pipeline UI contract.

Required on a model_pipeline, and not read by the engine for an application. This is what Studio renders as the model's form. The engine carries it opaquely and materialises it at install time as application/manifest.json, which is what the running model server reads and serves at GET /manifest.

Two things to know first. The engine layers identity onto the block on the way out: model_id is always forced to the package id, and display_name, version, description and license are filled in from the manifest's top level only where ui: has not set them. And the block is generated — its authoring source is model.meta.yaml, from which manifest.json is produced. The field names below are the generated ones, which is what a .hutash actually carries.

Model-level fields

FieldReq.What it does
model_idyesThe package id. Forced by the engine to match; a mismatch cannot survive.
display_nameyesRendered as the panel-header eyebrow (KOKORO, ACE-STEP). Falls back to model_id.
taglinenoOne-line marketing line for the marketplace card.
descriptionnoMulti-line description for the marketplace detail card.
license.spdxnoSPDX identifier. Drives the licence tier badge.
license.urlnoLink to the upstream licence text.
license.commercial_oknoWhether commercial use is permitted.
license.attribution_requirednoWhether attribution must be shown.
license.attribution_textnoThe exact attribution to display when required. null by default.
creator.name / .urlnoWho made the model, and their site.
source.upstream_reponoThe model's own code repository.
source.huggingface_reponoThe HuggingFace repo the weights come from.
hardware.min_vram_gbnoGraphics-memory floor. The engine falls back to this when the top-level min_vram_gb is 0.
hardware.recommended_vram_gbnoComfortable graphics memory.
hardware.supports_cpunoWhether the model runs without a GPU at all.
hardware.total_install_gbnoTotal disk the install consumes, weights included.
capabilitiesyesAt least one, keyed by capability id.
improvenoPrompt-improvement strategies. null hides the Improve button; absent falls back to a generic instruction.
labelsnoFree-form build provenance (hutash.model.hf_repo, hutash.runtime.backend, hutash.build.date, …). Informational.
notesnoAuthor's notes about the package. Informational.
api.healthnoThe model server's health path. Informational — the real health path is launch.yaml's health_endpoint.
api.gpunoWhether this build uses the GPU. Informational.
layoutnoWhich Studio page layout to mount. aggregator-3panel on every shipped pipeline. Validated backend-side against a closed list — not documented which values that list holds.
imageno{registry, name, tag, port} — the Docker image, from the container era. In the authoring schema; no shipped package carries it, because pipelines install into a Python virtual environment rather than pull an image.

Capabilities

ui.capabilities.<id>. The dict key becomes the modality the backend sees and the URL fragment the asset library uses. Studio's linter restricts it to tts, voice, music, stt, clone, voice-clone, sfx; adding another is a coordinated change across the modality map, the linter's valid-id list, the feature loader and the asset library's modality tokens.

FieldReq.What it does
labelyesThe capability's user-facing name ("Text to Speech", "Transcribe").
descriptionnoOne line describing what this capability does.
primarynoThe capability the model's page mounts by default. Default false.
endpointnoThe HTTP path on the model server this capability is served at (/tts, /stt).
status_messagenoText on the in-progress card while this capability runs. Defaults to "Generating…".
modalitynoPer-capability modality override for the asset-buffer and indexer paths. Defaults to the capability id. Chatterbox's voice-clone sets modality: clone for wire compatibility.
timeout_secondsnoThe model's typical-runtime ceiling. Informational only — it was once a hard read timeout, removed on the reasoning that the model is the authority on whether it is stuck, not a timer.
prompt_templatenoTemplate for the asset's display-name fallback when the user submits without typing a prompt. {filename} is substituted at the route layer; "Transcription of {filename}" is the canonical case.
inputssee noteThe artefacts the user supplies.
controlssee noteThe dials the user turns.
outputsnoWhat the model returns.

A capability must declare at least one input or one control; neither block is individually required.

Inputs

ui.capabilities.<id>.inputs.<input_id>. An input is the artefact the model operates on. The key becomes the form field name, the API request parameter, and the column in the asset's parameters JSON. Four types are valid; anything else is rejected by the linter, because no registered component means no rendering.

typeAliasesWhat it renders
stringtextA free-text field — the prompt textarea most generators use.
audio_fileaudioA drop-zone that takes an audio file.
image_fileimageA drop-zone that takes an image.
text_fileA drop-zone that takes a text file (a script, a subtitle).
FieldReq.What it does
typeyesOne of the four above.
labelyesThe visible field label. Must be unique across every capability of every model — duplicate labels make automated tests ambiguous.
requirednoWhether the form blocks submission without it. Default true.
descriptionnoHelp text under the label.
placeholdernoGreyed hint inside an empty text field.
max_lengthnoCharacter cap, string inputs.
acceptfile inputsAllowed MIME globs and extensions. A file-type input must list at least one MIME glob (audio/*) and at least one extension (wav or .wav): drag-time matching needs the glob, because browsers hide filenames during dragenter, and drop-time matching needs the extension.
clear_after_generatenotrue resets the field to its schema default after a successful run — per-job content. false keeps the value — a workspace setting. Default false.
rolenoPresent on Kokoro's prompt as role: prompt. Marks the field as the model's prompt for prompt-oriented features. not documented beyond that.
accept_filesnoExtensions the text field will also accept as a dropped file (txt, srt, md), so a prompt box doubles as a file drop.
accept_files_hintnoThe hint shown beside the field for that ("or drop a .txt / .srt file").
auto_fillno{via: stt} marks the input as auto-fillable by transcribing another input. Studio's transcriber service reads it and runs whisper-tiny behind the scenes. First consumer: dia-1.6b's voice_ref_transcript, filled from the uploaded voice clip.

Controls

ui.capabilities.<id>.controls.<control_id>. A control is a dial, not an artefact. Every control needs a type, a label and a default — use null for "no default".

Two vocabularies are in play. The specification names five closed semantic types: number, enum, string, boolean, file. Studio's runtime registry — what the linter enforces — is fifteen names: dropdown, select, text, text_input, string, textarea, slider, number, toggle, boolean, multi_select, file, file_upload, audio_file, image_file. Shipped packages use the registry — whisper-tiny writes type: enum and type: boolean, which resolve through the migration table (selectenum, toggleboolean). Author against the registry and the migration table together; the linter is what will actually stop you.

Shared fieldReq.What it does
typeyesOne of the registry types.
labelyesThe visible label. Must be unique across every capability of every model.
defaultyesThe starting value; null means no default. The linter checks the default's type against the control's type, and that a slider's default is inside [min, max].
descriptionnoHelp text under the label.
advancednotrue puts the control in the collapsible Advanced section.
null_labelnoThe label shown when the value equals a sentinel default — seed: -1 rendering as "Random".
implementation_statusnoactive | stub | deprecated. stub renders the control disabled with a "coming soon" cue. Default active.
clear_after_generatenotrue resets after a successful run; false persists, for a workspace setting tuned once. Default false.
TypeIts own fields
slidermin and max (both required), default inside that range, optional step and unit (a suffix shown next to the value).
select
alias dropdown, semantic name enum
options (required) and a default equal to one of the option values. Three option shapes are accepted: bare strings, a mapping of value to label, or an object list. In the object form only value is load-bearing; every other field is open-ended metadata (label, accent, gender, quality, language). A consumer that does not recognise a metadata field ignores it and falls back to label, or to value where there is none — so a plainer interface still constructs a correct invocation. Kokoro's 28-voice picker is why this form exists.
text_input
aliases string, text
default (use "" for empty), optional placeholder, and validation.min_length / .max_length / .pattern, all applied at submit.
textareaThe same fields as text_input, plus rows for the initial visible height.
toggle
alias boolean
default, a boolean.
numberdefault (number or null), optional min, max, step, unit. Use it where the user benefits from typing an exact value and may legitimately want one outside any obvious bound; use slider for a bounded value tuned by feel.
multi_selectoptions in the same three shapes, a default list whose entries are all valid option values ([] for none), and optional min_selections / max_selections.
file_upload
aliases file, audio_file, image_file
default must be null — files carry no serialisable default. accept is optional here and defaults to */*, unlike on the inputs side where it is required and shape-checked. max_size_mb is a soft cap that surfaces a warning rather than blocking.

Outputs

ui.capabilities.<id>.outputs.<output_id>. Outputs declare what the model returns and which renderer draws it.

YAML typeBecomesOther fields
wav, mp3, flac, oggaudio with that formatsample_rate
audioaudio, format optionalformat, sample_rate
png, jpeg, jpg, webpimage with that format
imageimage, format optionalformat
texttextformat (e.g. srt, vtt)
jsonjsonschema_hint
numbernumberunit

There is no top-level outputs: key on the package. Where a generated file lands is decided at download time by allowlist inversion, not by declaration: the shell checks the extension against a small stable list of model-weight formats (.safetensors, .ckpt, .pt, .pth, .gguf, .onnx, .bin) and routes a match into the shared blob store; everything else falls through to the user's own app output folder. A new output format needs no manifest change.

ui.improve

Controls the Improve button in the prompt textarea, which routes the prompt through a language model.

FieldWhat it does
defaultThe strategy id fired on a plain Improve click. Must be one of the declared strategy ids — the linter rejects a mismatch.
strategiesThe available strategies. Must be non-empty when improve is present.
strategies[].idStable identifier sent as strategy_id.
strategies[].labelThe label in the dropdown.
strategies[].instructionThe verbatim system prompt sent to the language model — what kind of improvement to make.

One strategy renders a plain "Improve" button; more than one renders "Improve ▾" with the default on a plain click. improve: null hides the button entirely — Whisper does this, because transcription has no prompt to improve. Omitting the key falls back to a generic "improve grammar and clarity" instruction, so ship it explicitly.

application/packages.yaml

Dependencies.

Present for any runnable package. The engine, not the package, selects a hardware variant at install time.

FieldDefaultWhat it does
python""The Python version for this package's virtual environment ('3.11', "3.12"). Quote it.
common[]Packages installed regardless of hardware. Ordinary requirement specifiers; a direct wheel URL is accepted.
variantsabsentPer-hardware alternatives, keyed gpu and cpu. Any other key is accepted but only used as a fallback.
variants.<key>.packages[]Packages installed only for that variant.
variants.<key>.indexes[]Extra pip index URLs needed to resolve them — the PyTorch CUDA index, a prebuilt-wheel index.
system_packages[]OS-level packages the build needs (build-essential, git, curl, espeak-ng).
requirements""Path to a requirements.txt inside the package, installed with uv pip install -r. This is the shape the two verticals use; auto-defaulted to requirements.txt for a git source that declares none.
extra_requirements[]Additional requirement files or specifiers layered on top.
frozenfalseThis list is a locked set to install with constraints, not requirements to resolve. Set on an external app whose list is a uv pip freeze snapshot captured against the pinned commit.
packages[]A flat dependency list, on the application side. Skipped entirely for a git source, whose own requirements file owns its dependencies.
preserve[]Paths inside an installed app that survive a reinstall — a user's custom nodes, extensions, outputs.

How the variant is chosen. With both gpu and cpu declared, gpu wins only when the host has a GPU and its VRAM meets the package's min_vram_gb; a package declaring no minimum is satisfied by GPU presence alone. Otherwise cpu is selected — deliberately, so a model whose minimum exceeds the card's actual memory installs CPU dependencies instead of GPU ones that would fail at load. With only one variant declared, that one is used regardless of hardware. With none, only common is installed.

frozen: true is specific. It is the dependency-graph equivalent of pinning a commit: capture once at test time, install as a locked set, never re-resolve. Re-freezing happens only when testing a new pinned commit. It is meaningless for the other source types — a self-bootstrapping app resolves nothing of ours, a PyPI source resolves fresh by design, and a local source is hand-written code.

application/launch.yaml

Starting a pipeline.

Model pipelines only. No shipped application package carries one.

FieldDefaultWhat it does
command""The executable to run. Every shipped pipeline uses uvicorn.
args[]Its arguments. {port} is substituted with the engine's pool assignment at launch.
env{}Environment variables for the model process. {model_dir} and {weights_dir} are substituted with the real paths.
port0A template anchor, not the port served on — the engine assigns from its own pool (49200–65535). Every shipped pipeline declares 8000.
health_endpoint""The path polled to decide the model has started. /health on every shipped pipeline.
health_timeout0Seconds to wait for that. 120 on every shipped pipeline.

The proven launch, identical in every shipped pipeline, runs the shared inference server rather than an entry point of the model's own.

application/launch.yaml
command: uvicorn args: - --factory - hutash_inference.server:create_app - --host - 127.0.0.1 - --port - '{port}' env: HUTASH_MODEL_ID: whisper-tiny HUTASH_MODEL_DIR: '{model_dir}' HF_HUB_CACHE: '{weights_dir}' HF_HUB_OFFLINE: '1' HUTASH_HF_REVISION: d90ca5fe260221311c53c58e660288d3deb8d356 port: 8000 health_endpoint: /health health_timeout: 120

Environment the shared server and inference.py read

VariableSet byWhat it does
HUTASH_MODEL_IDlaunch.yamlWhich model this process is.
HUTASH_MODEL_DIRlaunch.yaml, templatedWhere inference.py and manifest.json live.
HF_HUB_CACHElaunch.yaml, templatedWhere the downloaded weights are mounted.
HF_HUB_OFFLINElaunch.yaml"1" — never reach the network at load time.
HUTASH_HF_REVISIONlaunch.yamlThe exact weights commit. The resolver uses it to find the snapshot, and raises rather than falling back to "the only snapshot present", so a drifted mount fails loudly instead of loading the wrong weights.
HUTASH_DEVICEthe enginecuda | cuda:N | auto | cpu | mps.
HUTASH_VRAM_BUDGET_MBthe engineThe VRAM the model may claim in Low VRAM mode.
HUTASH_MODEL_FRAMEWORKthe enginehuggingface | llama-cpp | faster-whisper | custom — the shared server translates the budget into that framework's own knob, so inference.py needs no Low-VRAM code.
HUTASH_GPU_LAYERSthe shared serverThe llama.cpp layer count derived from the budget.
HUTASH_CONTEXT_LENGTHthe engineContext window for language models: the Settings override when set, otherwise a tier from total VRAM (under 4 GB → 2048, 4–8 GB → 4096, 8 GB+ → 8192; unmeasurable → 4096). A manifest-declared value always wins.

application/config/app.yaml

Starting an external application.

Applications only. Holds everything a pipeline splits across launch.yaml, plus the provenance a pipeline has no need for.

FieldReq.What it does
source.typeyesgit | self-bootstrap | pypi | local.
source.urlsee noteThe repository (for git / self-bootstrap) or the PyPI project page (informational, for pypi).
source.refnoBranch or tag. Informational only — never resolved. The commit is the lock.
source.commitconditionalThe exact commit to check out. Hard-required at parse time for git and self-bootstrap — the engine rejects those without one before any clone starts, because a ref alone is a floating pointer and two installs of "the same" entry would resolve differently on different days.
source.pathconditionalFor local: the folder beside the .hutash file to copy from. Not hard-validated, so a wrong value fails at install rather than at parse.
source.bootstrap_cmdnoself-bootstrap only — the app's own installer, which populates the shared virtual environment itself. Defaults to python launch.py --exit.
source.venv_dir_env_varnoself-bootstrap only — the variable the app's installer reads to find that environment. Defaults to VENV_DIR.
entrypointyesThe command that starts the app. A literal port number here is a template anchor: the engine rewrites --port 8100 to --port {port} when it registers the app, and substitutes its pool assignment at launch.
workdirnoThe working directory to launch from.
portsyesThe ports the app serves on. Real packages write - internal: 7860; a bare int, a string, or a list of ints also parse. The number must match the one in entrypoint — if they disagree the port rewrite never fires and every app tries to bind the same hardcoded port.
health.endpointnoThe path polled to decide the app is up. / for a Gradio app, /health for a FastAPI one. Accepts a bare string in place of the mapping.
health.pathnoAccepted alias for endpoint.
health.start_period_secondsnoThe grace window before health checks start counting. 60 for an app whose first start loads a model.
health.interval / .timeout / .start_period / .retriesnoFurther health-check tuning. Parsed, but not observed in any shipped package.
buildnoA post-clone step that runs before the app counts as installed — fetching weights the repo does not ship, or building a frontend. HivisionIDPhotos raises at startup with no weights present, so its build is what makes the install a working one.
env_varsnoEnvironment variables for the app process. {HUTASH_BASE} is expanded to the shared data root.
envnoThe same idea under the process spec. Shipped packages use env_vars.
managednoWhether the engine owns this process's lifecycle — starts it, stops it, injects its environment. Default false.
backendnovenv | container. Default venv.
system_packagesnoOS-level packages the install needs.
image / compute / mount_pathsnoContainer-runtime fields in the parser. No shipped application declares them.

application/config.yaml

Starting a vertical.

The two verticals carry this instead of config/app.yaml, and both files are identical.

application/config.yaml
port: 0 # 0 = engine assigns from pool (49200-65535) health: /health managed: true entrypoint: api/main.py
FieldWhat it does
portRequired. 0 asks the engine to assign one from its pool.
healthRequired. The health path, as a bare string.
managedRequired. true — the engine owns the process and injects its environment, which is how HUTASH_OS_PATH reaches the vertical.
entrypointRequired. The app's own module, relative to application/.

resources/weights.yaml

What to download.

Model pipelines only. Not used by applications, which manage any downloads of their own.

FieldReq.What it does
sourcesyesThe HuggingFace repositories to fetch.
sources[].repoyesThe repo id (Systran/faster-whisper-tiny). An entry with no repo is skipped silently.
sources[].revisionnoThe pinned commit. main is resolved to a commit hash at download time.
sources[].allow_patternsnoWhich files to fetch. Omit and the whole repo comes down — including demo audio, eval charts and documentation that is never read.
extranoCompanion repositories, same entry shape. Appended to the same weights list.
download_size_gbnoThe total download, shown before the user commits to it. Published to the index as disk_size_gb.

application/vertical.yaml

A whole interface as data.

Verticals only. This file is the frontend: the shared renderer reads it and draws navigation, page layouts, every widget, the Models marketplace and the Settings form. The vertical's own source holds a single <VerticalApp /> call and nothing else. A widget named here that the catalogue does not have renders an "unknown widget" note rather than silently nothing — which is the point: a vertical cannot quietly grow bespoke interface code.

Top level

KeyReq.What it does
app.titlenoThe shell header title. A vertical declaring no app: block renders no title.
app.descriptionnoA one-line description beside it.
pagesnoThe page-based interface. Absent falls back to the older single-workspace layout: block.
workflowsyesThe registry of what this app can run.
settingsyesThe settings schema, not its values. The Settings page is generated from exactly this.
layoutyesThe legacy single-workspace panel frame (type, left, center, right, each {content, width, label}). Superseded by pages:.
capabilitiesnoMirrors manifest.yaml's capabilities_needed; what the generated Models page filters to.
setupnoThe first-launch gate. Absent means no gate.
project.subfoldersnoFolders created inside every project.
project.transcript_pathnoThe one file the editor auto-saves and restores, and where a run's primary subtitle output is written. Declared once so the two sides cannot drift to separate files.
project.working_filenoThe one file a project's whole run is recorded in. Empty for a vertical that declares none — which is what left Podcast's show notes and chapters on disk and out of reach until it was added.
ui_overridesnoAccepted and otherwise ignored. The old slot-override mechanism was replaced by customWidgets, which overrides by widget type. The key stays so a parsed config still matches what the backend sends.

A page

KeyDefaultWhat it does
idrequiredThe page's identifier; every to: in a navigate action resolves against it.
labelIts name in the navigation bar.
iconIts glyph.
defaultfalseThe landing page.
hiddenfalseKeeps it out of the navigation while still reachable by navigating to it — which is how clicking a project opens the editor.
overlayfalseOpens on top of the current page, which stays mounted. Settings and Models are overlays so opening them from a half-edited transcript cannot unmount the editor and discard the edits.
overlay_sizemdmd | lg. lg for panels whose cards need the room — the marketplace, the caption gallery. Ignored without overlay.
dialogfalseAn overlay that draws its own content start to finish, with no title bar above it. The close button still renders.
reset_inputsinferredWhether opening this overlay clears the form. Unset, it is inferred from whether the page binds anything — right for a dialog collecting input for a new run, wrong for a panel over work already in progress. Set false on a panel whose page underneath still needs those inputs.
confirm_leaveThe question asked before navigation away commits.
layoutsinglesingle | two-panel | three-panel | editor. Anything else falls back to single.
left_width / right_widthColumn width. A bare number is a design unit at a 16px root (24015rem); a string passes through verbatim ("18rem", "65%").
top_height / middle_heightThe same, for the rows of an editor layout.
widgets[]What is on the page.
layoutValid position values
singlemain
two-panelleft, right, toolbar, middle
three-panelleft, center, right
editortoolbar, top, middle, bottom

A widget

KeyReq.What it does
typeyesSelects the component from the catalogue.
positionnoThe layout slot.
bindnoInput widgets only: the workflow input id this widget fills. The value goes into shared form state, which is how action_button finds it with nothing wired between them.
propsnoThe widget's own configuration. A prop the widget does not read is ignored — see the widget catalogue.

confirm_leave behaviour. Exactly one dialog, and it never changes shape while it is up. Nothing unsaved means no dialog at all, because a prompt on a departure that loses nothing is one users learn to dismiss without reading. Unsaved work with a project open offers Save and leave / Leave without saving / Cancel, where Save and leave navigates only if the write succeeded. Unsaved work with no project offers the warning and Leave / Cancel — there is no third button because there is nowhere to put the work. The auto-save debounce is held while the dialog is open, and the dialog freezes its decision when it opens, so a write landing mid-decision cannot rewrite the question under the button the user was reaching for.

A settings field

KeyReq.What it does
typeyesmodel_selector | dropdown | folder_picker are the types the two verticals use. The renderer falls back to a text input for an unrecognised type, on the reasoning that an unknown type is usually a typo and a visible field is a better signal than a missing one.
labelyesThe field label.
descriptionnoHelp text under it.
capabilitynomodel_selector only — which capability's models to offer.
optionsnodropdown only — {value, label} entries, plus any other fields the source declared.
defaultnoThe initial value.

Settings save per field. A single whole-form submit would send every field's last-rendered value, so editing one setting could quietly revert another.

A workflow registration, and the setup gate

KeyWhat it does
workflows[].idRequired. How an action_button names it.
workflows[].fileRequired. Path to the workflow YAML, relative to application/.
workflows[].labelRequired. Its display name.
workflows[].iconRequired. Its glyph.
workflows[].descriptionRequired. One line on what it does.
setup.required_capabilities[].capabilityRequired. The engine capability a model must declare.
setup.required_capabilities[].labelThe human name. Defaults to the raw capability id.
setup.required_capabilities[].recommended_modelOffered first. Falls back to the smallest listed.
setup.required_capabilities[].optionalOffered but never blocking — renders "Optional — skip for now."

vertical.yaml refines the gate; it never extends it. What the gate requires is derived from manifest.yaml's capabilities_needed, and an entry here for a capability the manifest does not declare is dropped rather than honoured — gating an app on something it never said it needed is a gate nobody can open. What counts as satisfied is at least one installed model declaring the capability: not a configured one, because settings can name a model since removed, and not a catalogue entry, because a thing you could install is not a thing you have. Two cases deliberately open the app rather than blocking it: no declared capabilities at all, and an unreachable engine.

application/workflows/*.yaml

What an app can do.

One file per workflow. The same YAML generates the web form, the CLI, and the tool surface — add an input here and it appears in all three.

KeyWhat it does
idMatches the id in vertical.yaml's workflows:.
nameDisplay name.
versionThe workflow's own version.
inputsWhat the run needs, keyed by input id.
stepsWhat it does, in order.
outputsWhat it produces, keyed by output id.

Inputs

KeyDefaultWhat it does
typerequiredfile | text | dropdown | slider | toggle | number | model_selector | folder_picker.
labelrequiredThe field label.
requiredtrueWhether the run blocks without it.
defaultnullThe initial value.
optionsnulldropdown only — {value, label} entries.
acceptnullfile only — MIME globs and extensions.
capabilitynullmodel_selector only.
min / max / stepnullslider and number.

Steps

KeyWhat it does
idRequired. How later steps refer to this one's result (${extract_audio.output}).
nameRequired. What the progress display calls it.
typeRequired. capability | ffmpeg | formatter | workflow | file_read | file_write.
everything elsePassed through to the step handler as config. Which keys matter depends on typecapability/model/input/params for a capability step, action/input/params for ffmpeg, format/input for a formatter, workflow/inputs for a sub-workflow.

Values interpolate: ${inputs.<id>} reads a run input, ${settings.<id>} reads a settings value, ${<step_id>.output} reads an earlier step's result, and ${<step_id>.outputs.<id>} reads a named output of a sub-workflow step.

Outputs

KeyDefaultWhat it does
typerequiredfile | text | subtitle_editor.
sourcerequiredThe variable reference this output's value comes from.
labelrequiredIts display name.
formatsnullThe formats it can be serialized to.
primaryfalseThis output is the workflow's deliverable — what an editor opens and an export writes when nothing names an output explicitly. At most one per workflow; none is fine for a single-output workflow. Without it a consumer can only guess by declaration order, which for transcribe-translate silently shows the untranslated text.
words_fromnullAnother output of this workflow holding the same cues with their per-word timings. Set when this output is a rendered string, because SRT and VTT cannot express a word time — so animated caption styles would otherwise have nothing to animate. Declared rather than discovered: in a dubbing workflow, "whatever other output holds segments" would find the original transcript and animate it over the translated line.

index.json

The catalogue.

One discovery surface over every package that exists. The engine fetches it once at startup and caches it in SQLite; every consumer filters the one cached copy by type rather than fetching its own. Studio filters pipeline; OS filters application.

The spec describes four fields per entry, on the principle that everything else lives in the package's own manifest. The shipped builder emits considerably more, and for a stated reason: a consumer building a full catalogue row from the index alone would otherwise have to fetch every package's zip, and the extra fields are read from the manifest the builder has already opened — so omitting them would not shrink the index, it would move the zip fetch downstream to break invisibly.

Identity fieldWhat it does
idThe package id, matching its own manifest.
typepipeline | application | plugin. Plugins are future — no plugin package exists. This is what every consumer filters on.
categoryA coarse grouping (audio, image, LLM, photo, …). For pipelines it doubles as the weights subfolder name.
hutashPath to the package's .hutash file, relative to the repo root, resolved against wherever the index was fetched from.

The derived display summary, also per entry: name, description, version, license, min_vram_gb, disk_size_gb, quality_score, speed, modality, hardware_label ("GPU (4GB+)" or "CPU", derived from gpu and min_vram_gb), internal, hf_repo, hf_revision, allow_patterns, weights_external. Four more appear only when the manifest declares them, because a default would be a claim: low_vram_capable, language, languages, min_os_version.

Top-level blockWhat it holds
modalitiesOne entry per generation modality: {color, label, subfolder} — the identity a modality carries across the interface, and the asset folder its outputs land in.
featuresThe interface routing table: {id, name, description, modality, primary_model_id, icon, version, layout, accepts, permissions} — which primary model handles which generation feature.

Both blocks ride alongside packages because they describe the generation system rather than any one package. The builder never regenerates their content; it carries them forward verbatim across a rebuild. Never hand-edit the packages list — run python scripts/build_index.py and --check it.