Configuration keys¶
Every key keryx reads, with its default and what happens when it is wrong. Keys are written here in dotted form; in YAML they nest. Where the files live and which layer wins is on the configuration page.
A key not listed here is not read. In particular, studio.port appears in some older
notes and is not a configuration key — the studio's port comes from --port only.
workspace.* — where reel workspaces live¶
| Key | Type | Default | Meaning |
|---|---|---|---|
workspace.root |
string | reels |
Directory, relative to the project root, holding one subdirectory per reel workspace. |
keryx reel new <slug> creates <workspace.root>/<slug>, and keryx reel list
enumerates that directory.
workspace.root is ignored by every command that takes --workspace
Passing --workspace <slug> binds the flag into the highest-precedence
configuration layer under the key workspace, which replaces the workspace
mapping the files supplied. workspace.root then resolves to empty and the command
falls back to the built-in reels.
So on a project that sets workspace.root, reel new and reel list use the
configured directory, while reel build, reel plan, reel lock, reel prune,
voice takes and every other --workspace command look under reels/ and report
workspace not found. reel prune <slug> given as a positional argument is
unaffected.
Until this is fixed, leave workspace.root unset and keep reel workspaces in
reels/.
reel.* — frame shape¶
| Key | Type | Default | Meaning |
|---|---|---|---|
reel.aspect |
string "W:H" |
9:16 |
Default frame shape for a reel. The short side is always 1080 px; the long side scales and rounds to an even number. |
A reel workspace can override this per reel with aspect in its workspace.yaml, and
reel build --aspect overrides both. An aspect more elongated than 3:1 in either
direction is rejected: aspect "5:1" is too elongated (beyond 3:1).
providers.* — selecting a backend¶
Each generative capability sits behind an interface, and the adapter is chosen by a scalar key. An empty or unset value selects the default; an unregistered name is an error listing what is available.
| Key | Default | Adapters actually registered |
|---|---|---|
providers.image |
gemini |
gemini |
providers.voice |
elevenlabs |
elevenlabs |
providers.music |
elevenlabs |
elevenlabs |
providers.render |
ffmpeg |
ffmpeg, afmpeg |
providers.video |
gemini |
none — video generation is not implemented, so resolving this capability always fails |
Chat is selected differently, by a nested key — see
providers.chat.* below.
The scalar and map forms of a providers key collide¶
providers.render is a scalar (the adapter name). providers.render.module is a key
inside a map. One YAML key cannot be both, and neither can two layers: the
higher-precedence layer's shape wins outright and the other form reads as empty.
# Rejected by the YAML parser — "mapping key render already defined".
providers:
render: afmpeg
render:
module: /opt/ffmpeg-wasi.wasm
Splitting them across ~/.keryx/config.yaml and the project .keryx.yaml does not
help either; whichever layer is higher supplies the shape and the other contributes
nothing.
The same collision applies to providers.image versus providers.image.price_per_image
and providers.image.currency, and to providers.voice versus
providers.voice.price_per_1k_chars. Choosing a non-default adapter and tuning that
adapter's per-unit price cannot both be expressed.
providers.render.* — the afmpeg wasm module¶
Read only when providers.render is not set as a scalar, for the reason above.
| Key | Type | Default | Meaning |
|---|---|---|---|
providers.render.module |
string | the pinned published module | A host path to a .wasm, or an https:// URL. A .gz URL is decompressed on download. |
providers.render.module_sha256 |
hex string | the pinned module's hash | SHA-256 of the decompressed .wasm, verified after download. |
providers.render.cache_dir |
path | OS user-cache directory | Where a downloaded module is cached. |
KERYX_FFMPEG_WASI is the env fallback for providers.render.module, and — because of
the collision — it is the only way to override the module while also selecting
providers.render: afmpeg.
With nothing set, the afmpeg backend downloads a pinned ffmpeg-wasi release (the
gpl variant, verified by SHA-256), caches it, and reuses it. You do not need to
fetch a module by hand.
providers.chat.* — the LLM for drafting and copy¶
| Key | Type | Default | Meaning |
|---|---|---|---|
providers.chat.provider |
string | none — see below | claude, claude-local, gemini, openai or openai-compatible. |
providers.chat.model |
string | the provider's own default | Model id passed to the chat client. |
There is no hardwired chat provider. When providers.chat.provider is unset, keryx
infers it from exactly one API key being present in the environment:
- one of
ANTHROPIC_API_KEY,GEMINI_API_KEYorOPENAI_API_KEYpresent → that provider is used; - none present →
no chat provider configured — set providers.chat.provider (claude|gemini|openai) + the matching API key; - more than one present →
multiple chat API keys present (…) — set providers.chat.provider to choose.
That last case is the one that catches people mixing vendors. Generating images with
Gemini needs GEMINI_API_KEY; if you also want chat from OpenAI you will have two keys
in the environment, and inference refuses rather than guessing. Set
providers.chat.provider: openai explicitly and both work — image and chat provider
selection are independent keys, and using different vendors for them is supported.
providers.gemini.* and providers.elevenlabs.* — adapter settings¶
| Key | Type | Default | Meaning |
|---|---|---|---|
providers.gemini.model |
string | the adapter's Imagen → Gemini fallback chain | Forces an image model id. Routed by prefix: imagen* uses the Imagen :predict endpoint, anything else :generateContent. |
providers.gemini.base_url |
URL | Google's endpoint | Override the API base. |
providers.elevenlabs.base_url |
URL | ElevenLabs' endpoint | Override the API base. |
cover --model and portrait --model override providers.gemini.model for one run.
providers.*.price_* — cost estimates¶
Prices are cues shown before you spend, not invoices. They are not vendor rates and are deliberately round.
| Key | Type | Default | Priced per |
|---|---|---|---|
providers.image.price_per_image |
float | 0.03 |
image |
providers.voice.price_per_1k_chars |
float | 0.30 |
1000 characters of narration |
providers.music.price_per_second |
float | 0.008 |
second of bed |
providers.chat.price_per_compose |
float | 0.01 |
social composition |
providers.image.currency |
string | USD |
the ISO code shown against every unit, not just images |
spend.* — the runaway-batch guard¶
| Key | Type | Default | Meaning |
|---|---|---|---|
spend.confirm_above.amount |
float | 10.0 |
Currency cap per request for flat-priced units (image, music, social). |
spend.confirm_above.characters |
float | 50000 |
Character cap per request for voice. |
Voice is measured on the character axis and everything else on the currency axis. Zero on an axis disables that axis.
themes.* — the style catalog¶
| Key | Type | Default | Meaning |
|---|---|---|---|
themes.default |
string | none | The single default theme keyword for every target (the current shape). |
themes.defaults.<type> |
string | none | Per-type default keyword, <type> being article, reel or portrait. The earlier shape, still read so an unmigrated catalog resolves. |
themes.<keyword> |
map | — | A theme definition. |
Themes are not seeded. On a fresh install there is no catalog, and the first
generation asks for one:
no theme configured for this type — add one with keryx theme add <keyword> --type <type>.
The first theme of a type becomes that type's default.
The field-by-field shape of a theme — palette, card treatment, scrim, music gain, voice clone settings — is on the themes page.
voices.* — the speaker registry¶
voices.<name> defines a named narration voice, referenced from a storyboard card's
voice.speaker for multi-author reels. The fields are the same as a reel theme's
voice block: id, stability, similarity, style, speed, speaker_boost,
model, pronunciation.
Naming a speaker that is not registered fails with
voice "<name>" not found (configure it under voices.<name>).
keryx init seeds a voice called matt carrying a real ElevenLabs voice id belonging
to the project's author. It is an example, not a default you want; delete it or replace
it with your own.
avatars.* — the actor registry¶
avatars.<name> defines a recurring character that can be composited into card
illustrations, selected per reel through workspace.yaml's avatars list and per card
through the storyboard's avatars field.
| Field | Meaning |
|---|---|
ref |
Character reference image, project-relative. |
likeness |
Theme-independent description of the person. |
style |
The avatar's own base art style, used when not restyling to a theme. |
palette |
The avatar's own palette, folded into style use. |
postamble |
Overrides the built-in prompt postamble (expression lock, no-text rule, 9:16 framing). |
Naming an unregistered avatar fails with
avatar not found: "<name>" (configure it under avatars.<name>).
As with voices, keryx init seeds an example avatar called matt describing the
project's author, whose ref path will not exist in your project.
log.level — logging verbosity¶
| Key | Type | Default | Meaning |
|---|---|---|---|
log.level |
string | info |
Framework log level. --debug overrides it for one run and survives a config reload. |
studio.* — the local web studio¶
| Key | Type | Default | Meaning |
|---|---|---|---|
studio.take_count |
int | 4 |
Candidate takes a studio generation produces. Clamped to 1–12; a value below 1 becomes 1, above 12 becomes 12. |
studio.takes_ttl |
duration | 3m |
Grace window before an unpicked in-memory candidate take is reclaimed. Accepts Go duration strings, e.g. 5m. |
studio.takes_ttl only bites on an in-memory project (a RAM worktree of a remote
git project). A local-disk project never prunes takes; its takes are ordinary files the
workspace lifecycle owns, cleared by reel prune. The
sweeper runs every 30 seconds and skips anything that is the picked slot, so
"generate a second round, then go back and pick from the first" works within the
window.
The studio's bind port and host come from --port and --host; there is no config key
for either.
git.* — the studio's git behaviour¶
| Key | Type | Default | Meaning |
|---|---|---|---|
git.commit_on_save |
bool | true |
The studio commits each saved change to the project repo. |
git.auto_push |
bool | false |
Exposed in the studio Settings panel as "push on save". Nothing reads it — setting it has no effect. |
storage.* — the media object store¶
Unset storage.provider means local-only, which is not an error: media stays in the
workspace and the sync commands report the store as unconfigured.
| Key | Type | Default | Meaning |
|---|---|---|---|
storage.provider |
string | unset | s3, gcs, azure, file or mem. |
storage.prefix |
string | empty | Project namespace inside a shared bucket. A blank prefix drops that path segment, giving keys of the form reels/<slug>/<relpath>. |
Credentials are never read from keryx config. Each backend resolves them through its own chain — the AWS default chain, Google ADC, the Azure default credential.
| Key | Applies to | Required | Meaning |
|---|---|---|---|
storage.s3.bucket |
s3 |
yes | Bucket name. Missing: storage.s3.bucket is not set. |
storage.s3.region |
s3 |
no | Overrides the AWS chain's region. |
storage.s3.endpoint |
s3 |
no | Endpoint for an S3-compatible store; setting it also switches to path-style addressing. |
storage.s3.profile |
s3 |
no | A named AWS profile. A profile name, not a credential. |
storage.gcs.bucket |
gcs |
yes | Bucket name. |
storage.azure.container |
azure |
yes | Container name. |
storage.azure.account |
azure |
no | Storage account name. |
storage.file.dir |
file |
yes | Local directory used as the object store. |
mem keeps objects in process memory and is there for tests; nothing survives the
process.
platforms.* — per-platform posting¶
platforms.<name>.enabled (bool, default false) gates a platform for
post and auth refresh.
<name> is instagram, youtube, tiktok or linkedin.
Credentials resolve environment variable → OS keychain → config key, in that order. The config fallback is plaintext; prefer the environment or the keychain. The full credential set-up per platform is in the how-to guides (Instagram, YouTube, TikTok, LinkedIn).
Non-secret platform settings:
| Key | Default | Meaning |
|---|---|---|
platforms.instagram.api_base |
https://graph.instagram.com/v21.0 |
API base URL. |
platforms.instagram.app_id, .user_id, .redirect_uri |
unset | Non-secret identifiers used by the OAuth flow. |
platforms.youtube.privacy |
private |
Upload privacy status. |
platforms.youtube.category_id |
22 (People & Blogs) |
YouTube category id. |
platforms.youtube.client_id, .channel_id |
unset | Non-secret identifiers. |
platforms.tiktok.api_base |
https://open.tiktokapis.com/v2 |
API base URL. |
platforms.tiktok.privacy |
SELF_ONLY |
Post visibility. Stays self-only until TikTok audits the app. |
platforms.tiktok.client_key, .open_id |
unset | Non-secret identifiers. |
platforms.linkedin.api_base |
https://api.linkedin.com |
API base URL. |
platforms.linkedin.api_version |
202606 |
LinkedIn versioned-API header. |
platforms.linkedin.visibility |
PUBLIC |
PUBLIC or CONNECTIONS. LinkedIn has no "only me". |
platforms.linkedin.client_id, .author_urn |
unset | Non-secret identifiers. |
platforms.<name>.constraints.* — overriding the composition limits¶
Each platform ships built-in norms used to steer composition and to gate approval. Every field can be overridden per platform, so a limit can be corrected without waiting for a release.
| Key | YouTube | TikTok | ||
|---|---|---|---|---|
constraints.text_cap |
2200 | 5000 | 2200 | 3000 |
constraints.title_cap |
0 (no title field) | 100 | 0 | 0 |
constraints.links_clickable |
false |
true |
false |
true |
constraints.hashtag_hint |
several–~10 (cap 30) | a few, in the description | a few, trend-led; front-load text | sparing (1–3); front-load ~140 |
Exceeding text_cap or title_cap is a hard violation: keryx approve refuses the
platform. A title on a platform with title_cap: 0, or a link on a platform with
links_clickable: false, is advisory — you get
instagram links are not clickable — use "link in bio" and posting still proceeds.
auth.writeback.* — where refreshed tokens are written¶
| Key | Type | Default | Meaning |
|---|---|---|---|
auth.writeback.backend |
string | local |
local writes rotated credentials back to ~/.keryx/accounts.yaml; gitlab writes them to a project's CI/CD variables. |
auth.writeback.gitlab.project |
string | CI_PROJECT_ID |
Project id or path to write variables into. |
auth.writeback.gitlab.api_url |
URL | CI_API_V4_URL, else https://gitlab.com/api/v4 |
GitLab API base. |
The gitlab backend needs GITLAB_TOKEN (api-scoped) in the environment and refuses
without it.
auth.alerts.* — expiry alerting¶
| Key | Type | Default | Meaning |
|---|---|---|---|
auth.alerts.backend |
string | none |
none, webhook or email. |
auth.alerts.webhook.url |
URL | unset | Webhook target. ALERT_WEBHOOK_URL takes precedence. |
auth.alerts.email.host |
string | unset | SMTP host. Required for email. |
auth.alerts.email.port |
int | 587 |
SMTP port. |
auth.alerts.email.from |
string | unset | Envelope sender. Required for email. |
auth.alerts.email.to |
list | unset | Recipients. Required for email. |
auth.alerts.email.username |
string | unset | SMTP user. ALERT_SMTP_USERNAME takes precedence. |
auth.alerts.email.password |
string | unset | SMTP password. ALERT_SMTP_PASSWORD takes precedence. |
webhook without a URL fails with
auth.alerts.backend=webhook but no URL — set ALERT_WEBHOOK_URL or auth.alerts.webhook.url;
email missing any of host, from or to fails with
auth.alerts.backend=email needs auth.alerts.email.{host,from,to}.
tls.source — the studio's certificate¶
| Key | Type | Default | Meaning |
|---|---|---|---|
tls.source |
string | localca |
localca provisions a browser-trusted per-machine certificate; anything else uses a self-signed one. |
An unknown value is not an error — it degrades to self-signed, and HTTPS is still
served so the Secure cookie holds. localca also falls back to self-signed when the
CA cannot be provisioned (no elevation, no certutil).
site.base_url — the canonical post URL¶
| Key | Type | Default | Meaning |
|---|---|---|---|
site.base_url |
URL | unset | Base URL of the site the reel points at. |
When set, storyboard draft rewrites the closing URL card to
<site.base_url>/<workspace slug> rather than trusting the host the model invented.
Unset, the model's text stands.