Skip to main content

Project file

A Kapi project is a portable kapi.yaml recipe at a project's root that captures a content workflow: source and target languages, content file patterns, tool pipelines (flows), plugin requirements, and processing defaults. It carries no credentials or state, so it is always safe to commit. For when to use a project versus an ad-hoc run, see Projects.

Discovery

Like git, kapi walks up the directory tree from where you run it and uses the nearest kapi.yaml recipe — running in project mode with that project's languages, defaults, and flows. A directory holds at most one kapi.yaml, so discovery is unambiguous. An explicit -p <path> overrides discovery, and KAPI_NO_PROJECT=1 disables it so the CLI runs ad-hoc.

Format

version: v1
name: My App

content:
- path: "src/locales/en/*.json"
target: "src/locales/{lang}" # mirror each matched file under the per-language dir

preset: nextjs
plugins:
okapi: "^1.47.0"

flows:
translate:
steps:
- tool: translate
config:
provider: anthropic
model: <provider-model-id>

translate-and-qa:
steps:
- tool: translate
config:
provider: anthropic
- tool: qa

pseudo:
steps:
- tool: pseudo-translate
config:
expansionPercent: 30

defaults:
source_language: en
target_languages:
- fr
- de
- ja
concurrency: 4
parallel_blocks: 3
encoding: utf-8
exclude:
- "**/*.generated.json"
merge:
conflict_policy: translator-wins
tm:
fuzzy_threshold: 75
segmentation:
source: true
termbase: terms/product.db

Fields

Required

FieldTypeDescription
versionstringSchema version, must be "v1"
namestringProject display name

Optional

FieldTypeDescription
contentContentEntry[]File patterns to process (see ContentEntry)
presetstringA framework preset name (see kapi init --list-presets) (e.g., nextjs, react-intl) applied to formats
pluginsmapPlugin requirements keyed by name (e.g., okapi: "^1.47.0")
requiresmapPlugin name → semver constraint that gates loading; the recipe refuses to load without a registered plugin
flowsmapNamed flow definitions (see Flow Steps)
defaultsDefaultsProcessing defaults (see Defaults; source/target languages live here)
ship_gateGateA single catch-all ship gate (see Ship gates)
ship_gatesShipGateRule[]Scoped ship-gate rules; the most specific matching rule wins (see Ship gates)
gatesmapNamed gate registry a ship_gates rule can reference by name (see Ship gates)
source_gateGateCoverage gate over the source authoring ladder (see source_gate)

ContentEntry

FieldTypeDescription
pathstringGlob pattern for source files (required)
basestringDirectory a matched file's path is made relative to; defaults to the glob's fixed prefix
formatstringFormat ID; auto-detected per file if omitted
targetstringOutput path template (see Source and target paths)

In a named collection, base may be set once on the collection and is inherited by every item that does not set its own.

Source and target paths

path is a doublestar glob: ** matches across directories (e.g. src/**/*.json) and {a,b,c} matches any of the listed alternatives (e.g. input/store/*.{json,yaml,html}). One glob can therefore cover a whole directory of mixed content; the format is auto-detected per file unless you pin format. Patterns resolve relative to the project root.

base is the directory a matched file's path is made relative to. It defaults to the glob's fixed prefix — the literal part of path before the first wildcard (input/docs/ for input/docs/**/*.md). The relative path drives the {relpath} / {path} / {dir} tokens and how much of the source tree a directory-mirror target reproduces.

target is the output-path template, expanded per source file and target language. The supported tokens are:

TokenMeaningExample (base input/, source input/docs/api.md)
{lang}Target languagefr
{relpath}Source path relative to base, with extensiondocs/api.md
{path}Source path relative to base, without extensiondocs/api
{dir}Directory portion of {relpath} (empty at the root)docs
{filename}Filename with extensionapi.md
{name}Filename without extension (alias {basename})api
{ext}Extension without the dotmd

A bare * is shorthand for {name}.

Directory-mirror (the easy default). When target ends with /, or its last segment has no extension and no wildcard/token, it names a directory and the source's {relpath} (under base) is mirrored beneath it. So target: output/{lang} reproduces the source tree under each per-language root — input/docs/api.mdoutput/fr/docs/api.md — with no * and no risk of a doubled extension:

content:
- name: Docs
base: input
items:
- path: "input/**/*.md"
target: "output/{lang}"

Token template (the powerful form). When you need to reshape the layout, use tokens explicitly:

content:
- path: "src/locales/en/*.json"
target: "src/locales/{lang}/{name}.{ext}" # api.json → src/locales/fr/api.json

Defaults

defaults holds project-wide processing settings that individual content items can override. Several of these are governed by AD-017.

FieldTypeDescription
source_languagestringBCP-47 source locale (e.g., en)
target_languagesstring[]BCP-47 target locales
locale_formatstringLocale style for the {lang} placeholder: bcp-47 (default) or posix
concurrencyintNumber of files to process in parallel
parallel_blocksintNumber of blocks to process in parallel within a file
encodingstringInput file encoding (default: utf-8)
excludestring[]Glob patterns skipped during content scanning (e.g. **/*.generated.json)
formatsmapPer-format preset, config, and detection priority overrides (see formats)
mergeMergeDefaultsHow kapi merge resolves a translator's target against an existing target or content-memory entry (see merge)
tmMemoryDefaultsContent-memory pre-fill on kapi extract and write-back on kapi merge (see tm)
segmentationSegmentationDefaultsOpt-in sentence-level segmentation overlay applied on extract (see segmentation)
redactionRedactionSpecReplace sensitive content with protected placeholders before processing and restore it afterwards (see redaction)
brand_voiceBrandVoiceBindingBind a brand voice profile as standing project context (see brand_voice)
termbasestringPath to a terms store, resolved relative to the project root; used for project-scoped term enforcement with no --termbase flag
termbase_sourcestringCommitted, git-tracked native source bundle (a .terms.json) the project terms store is compiled from; kapi apply edits it and re-imports into the gitignored terms cache, so git diff is the review surface. Unset, kapi falls back to <root>/terms.json, then <root>/.kapi/terms.json
tm_sourcestringCommitted, git-tracked native source bundle (a .memory.json) the project content memory is compiled from — the content-memory analogue of termbase_source. It has no conventional-location fallback: a project has one glossary but many memory bundles (one per content surface), so a seed is always named
statestringCommitted, git-tracked project state store (a kapi-project-state JSON document) — the authoritative carrier of per-unit workflow decisions (review ladder, approvals, parking) a plain target file can't hold. Empty defaults to .kapi-state.json; a server-backed project pushes state to a remote instead
flowstringThe project's default flow — the composed flow kapi up runs to catch up every locale. A built-in name (e.g. translate) or a key in flows:. Empty runs the built-in default (recycle → translate)
jobsintHow many target languages one kapi up pass runs concurrently. 0 (default) leaves it to the runner (4); --jobs overrides per run
source_gatestringSource-first convergence level: the source status a block must reach before it is translated (see source_gate)

Individual content items can override redaction per entry (ContentItem.redaction).

source_gate

defaults.source_gate is the source-first convergence gate: the source status a block must reach before the loop translates it. It gates the fan-out — distinct from the top-level source_gate coverage bar that kapi check --ship evaluates. Source-first convergence settles the source (terminology, brand, source QA) and holds the fan-out on it, so an unsettled, off-brand, un-term-checked source is never translated into every locale only to be redone when it changes. See Convergence — source first.

defaults:
source_gate: checked # the default: source cleared its automated terminology,
# brand, and source-QA checks (no human bottleneck)
# source_gate: approved # require a human/agent source sign-off first
# source_gate: none # opt out — raw fan-out, no source gate
ValueMeaning
(unset)Runner applies the default (checked)
authoredPresence baseline — any non-empty source qualifies
checkedDefault — source passed its automated terminology, brand, source-QA checks
approvedA human or agent signed off the source (brand-critical / regulated)
noneDeliberate opt-out — translate without settling the source first

On a connected project, a below-gate source holds the convergence run (source_not_ready) and opens a source-review task rather than translating; the source-side coverage bar and this convergence level are independent controls.

formats

defaults.formats configures formats by name, so a single wildcard content item (path: "input/*", no format:) can let each file's engine be auto-detected while you still tune individual formats in one place. Each entry takes:

FieldTypeDescription
presetstringA named format preset applied to this format (see kapi formats)
configmapReader option overrides for this format (merged under, and overridable by, a content item's format.config)
priorityintDetection priority — higher wins when several formats claim the same extension

priority is the engine-pin escape hatch. Most extensions map to one format, but some are claimed by several: .srt by both okf_vtt and okf_regex, .txt by several plain-text engines. Bumping the preferred one steers auto-detection without naming a format on every item:

plugins:
okapi-bridge: "*"
defaults:
formats:
okf_vtt:
priority: 110 # .srt → WebVTT engine (beats okf_regex)
okf_json:
config:
useFullKeyPath: true
content:
- name: Source Files
base: input
items:
- path: "input/*" # every file; engine auto-detected, two pinned above
target: "output/{lang}"

Plugin formats already outrank built-in ones by default (priority 100 vs 50), so declaring okapi-bridge is enough for input/* to resolve to the okf_* engines; priority only matters to break ties between formats from the same source.

merge

FieldTypeDescription
conflict_policystringtranslator-wins (default), existing-wins, or newest-wins

tm

FieldTypeDescription
fuzzy_thresholdintMinimum fuzzy match score (0–100) to pre-fill the target on extract (default: 75)
readstring[]Additional read-only content-memory files consulted during pre-fill; writes always go to the project content memory

segmentation

FieldTypeDescription
sourceboolToggle sentence-level segmentation of source text on extract
srxstringOptional SRX rules file; built-in default rules are used when empty

redaction

The sensitive term list lives in a separate rules file referenced by rules (so it can be gitignored); this spec points at it and selects detection backends.

FieldTypeDescription
enabledboolTurn redaction on for extract/merge
rulesstringPath to a redaction rules YAML file
detectorsstring[]Detection backends: rules and/or entities
placeholderstringOverride the visible stand-in template, e.g. [REDACTED:{category}]

brand_voice

Bind a brand voice profile under defaults.brand_voice. Exactly one source is expected.

FieldTypeDescription
profile_filestringPath to a standalone profile YAML, resolved relative to the project root
profilestringName of a profile in the local brand store
packstringName of a built-in starter pack

Flow Steps

Each flow contains an ordered list of steps. See Flow Steps Format for the full specification.

flows:
my-flow:
steps:
- tool: translate # tool name (required)
config: # tool-specific config (optional)
provider: anthropic
label: "Translate" # display label (optional)

Steps can also define parallel branches:

flows:
parallel-qa:
steps:
- parallel:
- tool: qa
- tool: qa
config:
provider: anthropic

Ship gates

Ship gates decide when translated content is shippable: coverage thresholds over the target lifecycle ladder (drafttranslatedreviewedsigned-off), enforced by kapi check --ship and driven toward by kapi up. The model — ladders, derived state, review — is the kapi loop; this section is the schema. All four keys are top-level (siblings of defaults) and optional: with none set, nothing is gated.

FieldTypeDescription
ship_gateGateA single catch-all gate: state name → threshold
ship_gatesShipGateRule[]Scoped rules, each a selector plus a gate; when present it replaces ship_gate
gatesmapNamed gate registry; a ship_gates rule references an entry with gate: <name>
source_gateGateA gate over the source authoring ladder (authoredcheckedapproved)

Gate and thresholds

A gate maps a lifecycle state to a threshold. A scope clears the gate when, for every threshold, at least that percent of its units are at that state or higher on the ladder. A threshold is written in one of two forms:

FormExampleMeaning
Shortreviewed: 80At least 80% at reviewed or higher; approver class defaults to human
Extendedreviewed: {pct: 80, by: any}pct (0–100) plus an explicit approver class by

pct must be 0–100; a threshold of 0 means "not required". by selects the approver class for the decision rungs (reviewed, signed-off): human (the default) counts only decisions made by a person or an agent acting for one, any also counts autonomous AI approvals. The short form always means by: human — an AI-approved unit never satisfies it. See approver classes for the rationale.

Simple form — one bar for the whole project:

ship_gate: { translated: 100, reviewed: 80 }

Every unit translated, the important 80% reviewed by a person; the long tail ships machine-translated.

Extended thresholds — the same shape with explicit pct/by:

ship_gate:
translated: 100
reviewed: { pct: 100, by: any }

Fully reviewed, but autonomous AI review decisions count toward the bar.

ShipGateRule

ship_gates is an ordered rule list. Each rule has:

FieldTypeDescription
whenSelectorOptional match: locales and/or collections (string lists). Omitted = catch-all default
gateGate or stringAn inline threshold map, or the name of an entry in the gates registry

An empty selector axis matches everything, so a rule with no when: is the default. When several rules match a (collection, locale) unit, the rule constraining the most axes wins wholesale; among equal specificity, the earliest-listed wins.

Scoped rules — different scopes answer to different bars:

ship_gates:
- when: { locales: [de] } # contractually reviewed market
gate: { translated: 100, reviewed: 100 }
- when: { collections: [Legal] } # regulated content, every locale
gate: { translated: 100, signed-off: 100 }
- gate: { translated: 100, reviewed: 80 } # the default

Named gates — define once in the gates registry, reference by name:

gates:
contractual: { translated: 100, reviewed: { pct: 100, by: human } }
machine: { translated: 100, reviewed: { pct: 100, by: any } }

ship_gates:
- when: { locales: [de, ja] }
gate: contractual
- gate: machine

A tiered per-market strategy built from these pieces is worked through in Tier gates per market.

source_gate

The source-side coverage gate: a threshold map over the authoring ladder (authoredcheckedapproved), gating the author's own content rather than the translations.

source_gate: { checked: 100 }

Every source unit clears its bound brand and terminology checks before kapi check --ship passes. Like ship_gate, the top-level source_gate is a coverage bar evaluated by kapi check --ship; source drift never blocks an ordinary build.

This is distinct from defaults.source_gate, the source-first convergence level (checked / approved / none) that decides whether the loop translates a block at all. The coverage bar answers is the source shippable?; the convergence level answers may this source be translated yet?.

Key Properties

  • No credentials — API keys are never stored in Kapi project files. They come from the OS keychain (Kapi Desktop) or environment variables (CLI).
  • No state — No sync cursors, caches, or timestamps. Kapi project files are always clean and safe to commit.
  • Portable — One kapi.yaml per project folder; share via git.
  • CLI-compatiblekapi run flowname -p kapi.yaml

Using with Kapi CLI

# Run a flow from a Kapi project
kapi run translate -p kapi.yaml

# Override defaults with CLI flags
kapi run translate -p kapi.yaml --target-lang de --provider openai

# One-shot mode still works without a project
kapi translate -i file.json --target-lang fr

Using with Kapi Desktop

  • File > Open to load a project by its folder (the one containing kapi.yaml)
  • File > New to create one from scratch
  • File > Save / Save As for standard document operations
  • Open a project by its folder rather than by double-clicking a branded file
  • Drag and drop a project folder onto the app window

See also

  • AD-008: Project model — the architectural decision behind the kapi.yaml recipe and its extension mechanism.
  • kapi.yaml Project File Format — the implementation note: the KapiProject, Defaults, and ContentCollection struct layouts, the named-collection content shape, validation rules, and how platform extensions decode unknown keys.
  • Flow Steps Format — the full step specification used by flows.