How env validation works
The runtime composes one big Zod schema at boot: the Tier-0 base schema (always required) plus every loaded plugin's configSchema. process.env is validated against the merged schema. Missing required vars fail boot with [boot-error] Plugin '<name>' env validation failed for '<field>'.
Disabling a plugin (via features or autoDetect) removes its env requirements automatically.
Tier-0 (core)
Always required by the runtime. Source: packages/oracle-runtime/src/config/base-env-schema.ts.
Runtime
| Variable | Type | Default | Notes |
|---|---|---|---|
NODE_ENV | 'development' | 'production' | 'test' | 'development' | |
PORT | number (coerced) | 3000 | Override at app.listen(port) if needed. |
ORACLE_NAME | string | — | Required. |
CORS_ORIGIN | string | '*' | Wildcard disables credentials. |
NETWORK | 'mainnet' | 'testnet' | 'devnet' | — | Required. |
Matrix
| Variable | Type | Default | Notes |
|---|---|---|---|
MATRIX_BASE_URL | string | — | Required. |
MATRIX_RECOVERY_PHRASE | string | — | Required. |
MATRIX_STORE_PATH | string | './matrix-storage' | Must persist across restarts. |
MATRIX_ORACLE_ADMIN_USER_ID | string | — | Required. |
MATRIX_ORACLE_ADMIN_PASSWORD | string | — | Required. |
MATRIX_ORACLE_ADMIN_ACCESS_TOKEN | string | — | Required. |
MATRIX_ACCOUNT_ROOM_ID | string | — | Required. |
MATRIX_VALUE_PIN | string | — | Required. |
Storage
| Variable | Type | Default | Notes |
|---|---|---|---|
SQLITE_DATABASE_PATH | string | — | Required. Must persist across restarts. |
Blocksync / chain
| Variable | Type | Default | Notes |
|---|---|---|---|
BLOCKSYNC_GRAPHQL_URL | string | — | Required. |
ORACLE_DID | string | — | Required. The oracle's own DID (did:ixo:ixo1...) — the signer identity used by the UCAN service and the auth middleware. |
ORACLE_ENTITY_DID | string | — | Required. The oracle's on-chain entity record DID. Distinct from ORACLE_DID — do not conflate them. |
SECP_MNEMONIC | string | — | Required. |
RPC_URL | string | — | Required. |
Auth / UCAN
Always present. Both are z.coerce.number() — set them as plain integer strings in .env.
| Variable | Type | Default | Notes |
|---|---|---|---|
UCAN_AUTH_MAX_TTL_SECONDS | number (coerced) | 900 | Max lifetime (seconds) the oracle accepts for a user auth invocation. Bounds the server-side replay window regardless of the TTL the client declares. Default 15 minutes. |
UCAN_REAUTH_PROMPT_THROTTLE_SECONDS | number (coerced) | 21600 | Throttle window (seconds) between "please re-authorize" prompts posted into a user's Matrix room when their stored delegation is missing or expired. Default 6 hours. |
LLM
| Variable | Type | Default | Notes |
|---|---|---|---|
LLM_PROVIDER | 'openrouter' | 'nebius' | 'openrouter' | |
OPENAI_API_KEY | string | — | Optional. |
OPEN_ROUTER_API_KEY | string | — | Required if LLM_PROVIDER=openrouter. |
NEBIUS_API_KEY | string | — | Required if LLM_PROVIDER=nebius. |
Cross-field check (validateLlmProviderKey): the API key for the selected LLM_PROVIDER must be present — OPEN_ROUTER_API_KEY when LLM_PROVIDER=openrouter (the default), NEBIUS_API_KEY when LLM_PROVIDER=nebius. A missing key fails boot with a named-field error (e.g. OPEN_ROUTER_API_KEY / NEBIUS_API_KEY) rather than a generic upstream 401 at request time. The per-role model ids are hardcoded per provider — there is no env var to swap the main model id (use the resolveModel hook for that).
Model-call liveness (Workers runtime)
@ixo/oracle-runtime-workers bounds every model call — main agent, sub-agents, helper models, platform and BYO alike — by watching the raw response bytes, so a stream that goes quiet fails within these budgets instead of running to the turn deadline (TURN_TIMEOUT_MS, 600 s). All four are optional.
| Variable | Type | Default | Notes |
|---|---|---|---|
LLM_HEADERS_TIMEOUT_MS | number (coerced) | 120000 | Time allowed for the response headers. |
LLM_STREAM_IDLE_TIMEOUT_MS | number (coerced) | 90000 | Longest silence between body bytes, and before the first one, on the platform lane (OpenRouter, Nebius). OpenRouter sends keep-alive comments while the model works, so a quiet stream really is stuck. |
LLM_BYO_STREAM_IDLE_TIMEOUT_MS | number (coerced) | 300000 | The same on BYO lanes (a user's own OpenAI, Anthropic, Gemini, DeepSeek or ChatGPT account), which may send nothing while a model reasons. |
LLM_STREAM_RETRIES | number (coerced) | 1 | Identical re-sends of a call that stalled before its first body byte, 0–5, clamped per lane so every attempt fits TURN_TIMEOUT_MS. A stall after bytes were received fails the call at once (kind: timeout, retryable). |
Any byte resets the idle clock and there is no cap on a call's total duration. The worst cases per lane, the retry rules and what the logs show are in the runtime's own docs: packages/oracle-runtime-workers/docs/configuration.md (Model-call liveness) and operations.md in the qiforge repository.
Run recovery (Workers runtime)
@ixo/oracle-runtime-workers 0.37.0 and later. A run whose isolate is reset mid-turn is recovered from its last checkpoint. Optional.
| Variable | Type | Default | Notes |
|---|---|---|---|
RUN_RECOVERY_TOTAL_ATTEMPTS | integer string, min 0 | 8 | How many times in all a run that was reset mid-turn is recovered, progress or not. After that the run ends interrupted and every surface (channel, Matrix room, Portal) gets a failure notice. This ends a turn that resets the isolate on every step. RUN_RECOVERY_ATTEMPTS (default 4) still bounds consecutive attempts without progress. |
The full recovery rules are in packages/oracle-runtime-workers/docs/operations.md in the qiforge repository. For the 128 MB isolate limit itself, see deploy → Cloudflare Workers runtime.
Output cap (Workers runtime)
@ixo/oracle-runtime-workers 0.39.0 and later. Optional.
| Variable | Type | Default | Notes |
|---|---|---|---|
LLM_MAX_OUTPUT_TOKENS | integer, min 0 | 16384 | Most tokens one platform model call (OpenRouter, Nebius) may generate, reasoning included. 0 leaves it to the model's own limit. Without a cap, a call that loops runs to that limit: 65,536 tokens on openai/gpt-5.6-luna. A reply cut off by the cap is kept as it stands. BYO lanes are not capped: the user's own key pays for them. |
Context and prompt cache (Workers runtime)
@ixo/oracle-runtime-workers 0.39.0 and later. All optional. Every model call re-sends the system prompt, the tool list and the thread, and the provider's prompt cache only helps while that prefix stays the same. So the runtime saves changes to the prefix for a cold turn: the first turn of a thread, or a turn that opens CONTEXT_CACHE_COLD_MS or more after the thread's last activity, when the cache has expired anyway. Warm turns never unload a capability or trim old results. See Loading and unloading.
| Variable | Type | Default | Notes |
|---|---|---|---|
CONTEXT_CACHE_COLD_MS | integer string, min 0 | 2100000 (35 min) | Quiet time after which the next turn is cold. 0 makes every turn cold. |
CONTEXT_STALE_RESULT_TURNS | integer string, min 0 | 2 | On a cold turn, tool results older than this many finished turns go to the model as one-line stubs; read_result reads one back whole. Only the request changes: the checkpoint and the transcript keep every result. 0 keeps them all. |
CONTEXT_PLUGIN_IDLE_TURNS | integer string, min 0 | 3 | On a cold turn, loaded capabilities that none of this many latest turns used are unloaded. The model can load one again with load_capability. 0 keeps them all. |
CONTEXT_SUMMARIZE_MAX_TOKENS | integer string, min 1000 | 200000 | Ceiling on when a long history is summarized. Summarizing still starts at 60% of the model's window (CONTEXT_SUMMARIZE_FRACTION) when that is lower. |
CONTEXT_PRUNE_MAX_TOKENS | integer string, min 1000 | 100000 | Ceiling on when old tool results are replaced by placeholders under pressure. Pruning still starts at 35% of the window (CONTEXT_PRUNE_FRACTION) when that is lower. |
CONTEXT_RESULT_CAP_MAX_CHARS | integer string, min 4000 | 64000 (200000 before 0.39.0) | Largest single tool result the model gets, in characters. The full result is stored and reachable through read_result. |
The two token ceilings are about cost, not fit: on a large window they stop the runtime from re-sending a very long thread on every call. The other CONTEXT_* variables and the reasons behind each default are in packages/oracle-runtime-workers/docs/configuration.md in the qiforge repository.
Capability router (Workers runtime)
The capability router predicts which on-demand capabilities a message needs and loads them before the first model call, which saves the model a load_capability round trip. It needs a DECISION_PROVIDER. The thresholds are @ixo/oracle-runtime-workers 0.39.0 and later.
| Variable | Type | Default | Notes |
|---|---|---|---|
CAPABILITY_ROUTER | 'off' | 'shadow' | 'on' | 'off' | on waits up to one second for the verdict and then loads every predicted capability on the thread. shadow evaluates without loading anything and logs what it would have loaded ([capability-router-shadow]), to measure accuracy. Any failure loads nothing. |
CAPABILITY_ROUTER_WARM_THRESHOLD | number, min 0 | 0.8 | Score a capability needs to be loaded on a warm turn, where a wrong load costs a full prompt-cache miss. Above 1, the router does not run on warm turns. Provisional. |
CAPABILITY_ROUTER_COLD_THRESHOLD | number, min 0 | 0.7 | Score a capability needs on a cold turn. Provisional. |
The router sends the user's message and the thread's last four visible turns, shortened, to the Decision provider. How it picks candidates is in Meta-tools and loading.
Misc
| Variable | Type | Default | Notes |
|---|---|---|---|
ORACLE_SECRETS | string | '' | Comma-separated KEY=value pairs surfaced as x-os-* headers to capabilities. |
LIVE_AGENT_AUTH_API_KEY | string | '' | Optional. |
LangSmith tracing (optional)
| Variable | Type | Default | Notes |
|---|---|---|---|
LANGSMITH_TRACING | string | — | Set to 'true' to enable. |
LANGSMITH_API_KEY | string | — | |
LANGSMITH_PROJECT | string | — | |
LANGSMITH_ENDPOINT | string | — | Optional override of the LangSmith API URL. |
Declared in the base schema so they show up in qiforge-cli env output. LangChain auto-wires when these are present in process.env; the runtime never reads them directly.
Per-plugin
Only required when the named plugin is loaded.
| Plugin | Variable | Required | Notes |
|---|---|---|---|
memory | MEMORY_MCP_URL | Yes | Must be a valid HTTP(S) URL. |
memory | MEMORY_ENGINE_URL | Yes | Must be a valid HTTP(S) URL. |
memory | ORACLE_ENTITY_DID | Yes (@ixo/oracle-runtime-workers 0.24.0 and later) | did:ixo:entity: followed by 32 lower-case hex characters. Sent to the Memory Engine as x-oracle-entity-did; the engine (2.0.0 and later) refuses requests without it. See memory. |
firecrawl | FIRECRAWL_MCP_URL | Yes | Must be a valid HTTP(S) URL. |
domain-indexer | DOMAIN_INDEXER_URL | No | URL override; otherwise resolved from NETWORK. |
composio | COMPOSIO_API_KEY | Yes (when loaded) | |
composio | COMPOSIO_BASE_URL | No | Defaults to https://composio.ixo.earth. |
sandbox | SANDBOX_MCP_URL | Yes | Must be a valid URL. |
skills | SKILLS_CAPSULES_BASE_URL | No | URL. Defaults to https://capsules.skills.ixo.earth. |
slack | SLACK_BOT_OAUTH_TOKEN | Yes | Triggers autoDetect. |
slack | SLACK_APP_TOKEN | No | |
slack | SLACK_USE_SOCKET_MODE | No | Defaults to 'true'. |
slack | SLACK_MAX_RECONNECT_ATTEMPTS | No | Coerced to number; default 10. |
slack | SLACK_RECONNECT_DELAY_MS | No | Coerced to number; default 1000. |
credits | SUBSCRIPTION_URL | No | URL. |
credits | SUBSCRIPTION_ORACLE_MCP_URL | No | URL. |
credits | DISABLE_CREDITS | No | Enum — exactly 'true' or 'false' (any other value fails env validation). 'true' disables the credit-enforcement middleware; the plugin's autoDetect also excludes the whole plugin when DISABLE_CREDITS=true. |
tasks | REDIS_URL | Yes (when loaded) | Triggers autoDetect. |
tasks | TASKS_MAX_PER_USER | No | Coerced positive int; default 50. Max scheduled tasks per user. |
tasks | TASKS_RUN_LOCK_TTL_SEC | No | Coerced positive int; default 600. Per-run lock TTL (seconds). |
tasks | TASKS_MIN_CRON_INTERVAL_SEC | No | Coerced positive int; default 300. Minimum allowed cron interval (seconds). |
matrix-group-chats | CHANNEL_MEMORY_SYNC_INTERVAL_MS | No | Coerced int, min 1000; default 60000. Debounce window between a write and the Matrix snapshot upload. |
matrix-group-chats | GROUP_CHAT_ACTIVE_THREAD_TTL_MS | No | Coerced int, min 60000; default 1800000. How long a thread stays "active with the bot" after a reply. |
matrix-group-chats | GROUP_CHAT_REQUIRE_POWER_LEVEL | No | Coerced int, min 0; default 0. Extra minimum power level the bot must have before posting (0 = use the room default). |
matrix-group-chats | GROUP_CHAT_ROOM_INFO_TTL_MS | No | Coerced int, min 60000; default 1800000. How long roomInfo (membership, DM flag) stays cached. |
vfs | VFS_MAX_READ_LINES | No | Coerced positive int; default 2000. Max lines a single vfs_read window returns. |
vfs | VFS_REQUEST_TIMEOUT_MS | No | Coerced positive int; default 20000. Per-request timeout to the VFS worker. |
ixo-chain | IXO_CHAIN_TOOLS | No | Workers runtime (@ixo/oracle-runtime-workers 0.28.0 and later). on (default) or off. off leaves the plugin out of the oracle. |
ixo-chain | CHAIN_REST_URL | No | Workers runtime (0.28.0 and later). Chain REST (LCD) base URL for balances, denom metadata, the overview's chain sections and the chain kinds of chain_lookup: https, no credentials, query or fragment (a /rest path is fine). Defaults per NETWORK: mainnet https://impacthub.ixo.world/rest, testnet https://testnet.ixo.earth/rest, devnet https://devnet.ixo.earth/rest. See ixo-chain. |
ixo-chain | IXO_CHAIN_IBC_DENOMS | No | Workers runtime (0.28.2 and later). More verified IBC tokens, merged over the built-in ones of the oracle's NETWORK. Invalid JSON or an invalid entry fails boot validation. See ixo-chain. |
ixo-chain | IXO_NAMES_URL | No | Workers runtime (0.34.0 and later). The IXO Names worker's origin: https, no credentials, query or fragment. Defaults per NETWORK: mainnet https://names.ixo.earth, testnet https://testnet.names.ixo.earth, devnet https://devnet.names.ixo.earth. See Names. |
ixo-chain | IXO_NAMES | No | Workers runtime (0.34.0 and later). on (default) or off. off leaves names out of every answer and asks the worker nothing. |
Variables read but not owned
Some plugins read variables that live in another schema:
composioreads the coreNETWORKand forwards it asx-ixo-network.skillsreadsNETWORKand forwards asX-IXO-Network.sandboxreadsORACLE_SECRETS(core) andSKILLS_CAPSULES_BASE_URL(owned byskills) and forwards them as headers.vfsreads the coreNETWORK(selects the bundled VFS + UCAN Store worker URLs — nothing to configure) andSANDBOX_MCP_URL(owned bysandbox; when set, adds the two sandbox↔files bridge tools).ixo-chainreads the coreNETWORK(selects the default chain REST gateway and Names worker, and is named in every result) andBLOCKSYNC_GRAPHQL_URL(the IXO Blocksync endpointchain_lookupqueries).
These are declared in the plugin's sibling schemas (typed optional()); a missing value just skips the matching header instead of failing the plugin build.
Generating an .env template
qiforge-cli env generates a .env template for your installed plugin set. Until that lands, write the file by hand using this reference.
Related references
- Plugin catalog — env vars per plugin in catalog form.
- Plugin config and env guide — declaring your own plugin's vars.