Skip to content

Configuration Files Reference

Authoritative schema reference for everything under /data/config/. This page is the agent's map of its own configuration — every key, type, default, and validation rule, with links to the Settings UI documentation for behavioral context.

If you are an end user, prefer the Settings overview and its subpages — they explain what each setting does. This page explains exactly what shape the file on disk has.

Editing rules of thumb

  • Web UI is the supported path for everything except AGENTS.md / HEARTBEAT.md / CONSOLIDATION.md (those have their own Instructions editor).
  • Stop the container before hand-editing JSON — the backend keeps an in-memory copy and overwrites the file on the next save through the UI.
  • All non-secret values are merged via PUT /api/settings which applies per-field validation; raw file edits bypass that validation.

Files in /data/config/

FileCreated byEdited byEncrypted?Schema
settings.jsonensureConfigTemplates() (packages/core/src/config.ts) on first bootUI Settings panels (all except Telegram & Secrets)NoSettingsContract
telegram.jsonensureConfigTemplates() on first bootUI Settings → Telegram, Telegram users panelNo (token plain text)TelegramSettingsStorageContract
providers.jsonensureConfigTemplates() on first bootUI Providers pageYes (apiKey, oauthCredentials)ProvidersFile
secrets.jsonFirst call to saveSecrets()UI Settings → SecretsYes (every value)SecretsFile
skills.jsonensureConfigTemplates() on first bootUI Skills pageYes (env values)SkillsFile
AGENTS.mdSeeded by Instructions defaultsUI Instructions pageNoFree-form Markdown
HEARTBEAT.mdSeeded by Instructions defaultsUI Instructions pageNoFree-form Markdown
CONSOLIDATION.mdSeeded by Instructions defaultsUI Instructions pageNoFree-form Markdown

The directory itself is created with 0700 mode the first time the backend runs. See File Paths for the volume mapping.


settings.json

The non-secret runtime config. Edited via every Settings UI panel except Telegram (which writes to telegram.json) and Secrets (which writes to secrets.json).

The on-disk shape is a superset of SettingsContract (packages/core/src/contracts/settings.ts): the contract describes what the API exchanges with the frontend; the file may also contain a few keys the UI doesn't surface (tokenPriceTable, builtinTools, braveSearchApiKey, searxngUrl) plus dead fields from older versions until they get re-saved.

Top-level keys

JSON pathTypeDefaultValidationControls
sessionTimeoutMinutesnumber30> 0Inactivity window before a chat session ends — see Memory → Session timeout.
sessionSummaryProviderIdstring""stringProvider id (provider::model) used to summarize ended sessions; "" = active provider.
languagestring"en" (template) / "match" (normalized)non-emptyForced reply language or "match" — see Agent → Language.
timezoneIANA tz string"UTC"non-emptyCron + daily-memory naming. Overridden by TZ env var if set. See Agent → Timezone.
thinkingLevel"off" | "minimal" | "low" | "medium" | "high" | "xhigh""off"enumReasoning effort for the chat agent — see Agent → Thinking level.
healthMonitorIntervalMinutesnumber5> 0Health-check frequency — see Health Monitor → Interval.
uploadsobjectsee belownestedUpload retention policy.
healthMonitorobjectsee belownestedProvider health checks + fallback.
memoryConsolidationobjectsee belownestedNightly memory job.
factExtractionobjectsee belownestedPer-session fact extraction.
agentHeartbeatobjectsee belownestedBackground reflection loop.
tasksobjectsee belownestedTask & cronjob defaults.
ttsobjectsee belownestedVoice output config.
sttobjectsee belownestedVoice input config.
tokenPriceTableRecord<string, { input: number; output: number }>seed pricesnot validated by APIPer-model USD/1M-token costs used by Token Usage. Merged on top of DEFAULT_PRICE_TABLE.
builtinToolsobjectsee belownot validated by APIEnable/disable built-in web_search and web_fetch tools, choose the search provider.
braveSearchApiKeystring""not validated by APILegacy — read at boot, migrated into builtinTools.webSearch.braveSearchApiKey if present.
searxngUrlstring""not validated by APILegacy — read at boot, migrated into builtinTools.webSearch.searxngUrl if present.
heartbeatobjectsee templatenot read at runtimeLegacy / dead key. Seeded by the template, never read by current code. Replaced by healthMonitor + healthMonitorIntervalMinutes on the first save. Safe to delete by hand.

uploads

KeyTypeDefaultRangeUI
uploads.retentionDaysnumber30>= 0 (0 = delete on next cleanup)Agent → Upload retention
json
{ "uploads": { "retentionDays": 30 } }

healthMonitor

Health Monitor settings UI.

KeyTypeDefaultValidation
healthMonitor.enabledbooleantruecoerced to bool
healthMonitor.fallbackTrigger"down" | "degraded""down"enum
healthMonitor.failuresBeforeFallbacknumber1> 0
healthMonitor.recoveryCheckIntervalMinutesnumber1> 0
healthMonitor.successesBeforeRecoverynumber3> 0
healthMonitor.notifications.healthyToDegradedbooleanfalsebool
healthMonitor.notifications.degradedToHealthybooleanfalsebool
healthMonitor.notifications.degradedToDownbooleantruebool
healthMonitor.notifications.healthyToDownbooleantruebool
healthMonitor.notifications.downToFallbackbooleantruebool
healthMonitor.notifications.fallbackToHealthybooleantruebool

Legacy nested intervalMinutes

The PUT endpoint also accepts healthMonitor.intervalMinutes and migrates it to the top-level healthMonitorIntervalMinutes (see withLegacySettingsPayloadCompatibility). New writes always go to the top-level field.

memoryConsolidation

Memory → Memory consolidation UI.

KeyTypeDefaultRange
memoryConsolidation.enabledbooleantruebool
memoryConsolidation.runAtHournumber30–23 (local time, see timezone)
memoryConsolidation.lookbackDaysnumber3integer 1–30
memoryConsolidation.providerIdstring""string ("" = active provider)

Companion file: /data/config/CONSOLIDATION.md — the prompt for the consolidator. See Instructions → CONSOLIDATION.md.

factExtraction

Memory → Fact extraction UI.

KeyTypeDefaultRange
factExtraction.enabledbooleantruebool
factExtraction.providerIdstring""string
factExtraction.minSessionMessagesnumber3integer 1–100

agentHeartbeat

Agent Heartbeat UI.

KeyTypeDefaultRange
agentHeartbeat.enabledbooleanfalsebool
agentHeartbeat.intervalMinutesnumber60> 0 (UI clamps 1–1440)
agentHeartbeat.nightMode.enabledbooleantruebool
agentHeartbeat.nightMode.startHournumber230–23
agentHeartbeat.nightMode.endHournumber80–23

startHour > endHour (e.g. 23 → 7) means the window crosses midnight — that is the expected case.

Companion file: /data/config/HEARTBEAT.md — the prompt the agent receives every tick.

tasks

Tasks UI.

KeyTypeDefaultRange / enum
tasks.defaultProviderstring (provider::model)""string
tasks.maxDurationMinutesnumber60> 0 (UI clamps 1–1440)
tasks.telegramDelivery"auto" | "always""auto"enum
tasks.backgroundThinkingLevelthinking-level enum"off"same as top-level thinkingLevel
tasks.loopDetection.enabledbooleantruebool
tasks.loopDetection.method"systematic" | "smart" | "auto""systematic"enum
tasks.loopDetection.maxConsecutiveFailuresnumber3> 0
tasks.loopDetection.smartProviderstring""string
tasks.loopDetection.smartCheckIntervalnumber5> 0
tasks.statusUpdates.enabledbooleanfalsebool
tasks.statusUpdates.intervalMinutesnumber10integer 1–120

Legacy tasks.statusUpdateIntervalMinutes

A flat tasks.statusUpdateIntervalMinutes may exist on disk from older installs. PUT still accepts it (validated as integer 1–120) and writes it through to tasks.statusUpdates.intervalMinutes without flipping enabled to true — opting in is explicit. After the next save, the flat key is no longer needed.

tts

Text-to-Speech UI.

KeyTypeDefaultValidation
tts.enabledbooleanfalsebool
tts.provider"openai" | "mistral""openai"enum (SETTINGS_TTS_PROVIDERS)
tts.providerIdstring (provider entry id)""string
tts.openaiModel"gpt-4o-mini-tts" | "tts-1" | "tts-1-hd""gpt-4o-mini-tts"enum (SETTINGS_TTS_OPENAI_MODELS)
tts.openaiVoicestring (e.g. "nova", "alloy")"nova"non-empty string
tts.openaiInstructionsstring""string
tts.mistralVoicestring (e.g. "nadia-neutral")""string
tts.responseFormat"mp3" | "wav" | "opus" | "flac""mp3"enum (SETTINGS_TTS_RESPONSE_FORMATS)

stt

Speech-to-Text UI.

KeyTypeDefaultValidation
stt.enabledbooleanfalsebool
stt.provider"whisper-url" | "openai" | "ollama""whisper-url"enum (SETTINGS_STT_PROVIDERS)
stt.whisperUrlstring (full URL)""string
stt.providerIdstring (provider entry id)""string
stt.openaiModel"whisper-1" | "gpt-4o-transcribe" | "gpt-4o-mini-transcribe""whisper-1"enum (SETTINGS_STT_OPENAI_MODELS)
stt.ollamaModelstring""string
stt.rewrite.enabledbooleanfalsebool
stt.rewrite.providerIdstring (provider::model composite)""string

tokenPriceTable

json
{
  "tokenPriceTable": {
    "gpt-4o":                       { "input": 2.5,  "output": 10.0 },
    "gpt-4o-mini":                  { "input": 0.15, "output": 0.6 },
    "claude-3-5-sonnet-20241022":   { "input": 3.0,  "output": 15.0 },
    "claude-sonnet-4-20250514":     { "input": 3.0,  "output": 15.0 }
  }
}
  • Keys are model ids exactly as they appear in pi-ai responses.
  • input / output are USD per 1M tokens.
  • The runtime merges this object on top of DEFAULT_PRICE_TABLE — entries you list here override the defaults; missing entries fall back to the defaults; entries unknown to either are reported as cost null in Token Usage.
  • Not exposed by the Settings UI — edit directly. Read by getConfiguredPriceTable() (packages/core/src/provider-config.ts).

builtinTools

json
{
  "builtinTools": {
    "webSearch": {
      "enabled": true,
      "provider": "duckduckgo",
      "braveSearchApiKey": "",
      "searxngUrl": ""
    },
    "webFetch": { "enabled": true }
  }
}
KeyTypeDefaultNotes
builtinTools.webSearch.enabledbooleantrueWhen false, the web_search tool is not registered.
builtinTools.webSearch.provider"duckduckgo" | "brave" | "searxng""duckduckgo"DuckDuckGo is keyless. brave requires braveSearchApiKey. searxng requires searxngUrl.
builtinTools.webSearch.braveSearchApiKeystring""Encrypted at rest by web-tools.ts if you write it via that path; the file form is plain text — prefer keeping this in secrets.json and referencing via env.
builtinTools.webSearch.searxngUrlstring""Base URL of a SearXNG instance.
builtinTools.webFetch.enabledbooleantrueWhen false, the web_fetch tool is not registered.

The runtime also accepts the legacy flat keys braveSearchApiKey and searxngUrl at the top level of settings.json; on boot they are folded into builtinTools.webSearch if the new keys are empty (packages/web-backend/src/bootstrap/runtime-composition.ts).

Default settings.json (after first boot)

This is the literal file written by ensureConfigTemplates():

json
{
  "sessionTimeoutMinutes": 30,
  "sessionSummaryProviderId": "",
  "language": "en",
  "timezone": "UTC",
  "thinkingLevel": "off",
  "heartbeat": {
    "intervalMinutes": 5,
    "fallbackTrigger": "down",
    "failuresBeforeFallback": 1,
    "recoveryCheckIntervalMinutes": 1,
    "successesBeforeRecovery": 3,
    "notifications": {
      "healthyToDegraded": false,
      "degradedToHealthy": false,
      "degradedToDown": true,
      "healthyToDown": true,
      "downToFallback": true,
      "fallbackToHealthy": true
    }
  },
  "uploads": { "retentionDays": 30 },
  "tokenPriceTable": {
    "gpt-4o":                     { "input": 2.5,  "output": 10 },
    "gpt-4o-mini":                { "input": 0.15, "output": 0.6 },
    "claude-3-5-sonnet-20241022": { "input": 3,    "output": 15 },
    "claude-sonnet-4-20250514":   { "input": 3,    "output": 15 }
  },
  "memoryConsolidation": { "enabled": true, "runAtHour": 3, "lookbackDays": 3, "providerId": "" },
  "factExtraction":      { "enabled": true, "providerId": "", "minSessionMessages": 3 },
  "agentHeartbeat": {
    "enabled": false,
    "intervalMinutes": 60,
    "nightMode": { "enabled": true, "startHour": 23, "endHour": 8 }
  },
  "builtinTools": {
    "webSearch": { "enabled": true, "provider": "duckduckgo" },
    "webFetch":  { "enabled": true }
  },
  "braveSearchApiKey": "",
  "searxngUrl": "",
  "tasks": {
    "defaultProvider": "",
    "maxDurationMinutes": 60,
    "telegramDelivery": "auto",
    "loopDetection": {
      "enabled": true,
      "method": "systematic",
      "maxConsecutiveFailures": 3,
      "smartProvider": "",
      "smartCheckInterval": 5
    },
    "statusUpdates": { "enabled": false, "intervalMinutes": 10 },
    "backgroundThinkingLevel": "off"
  }
}

Note: tts, stt, healthMonitor, and healthMonitorIntervalMinutes are not in the template — they are added the first time the user saves the relevant Settings panel. Until then, the runtime falls back to the defaults documented above (and surfaced by mapSettingsResponse).


telegram.json

Stored separately from settings.json (the Settings UI groups it under the Telegram panel, but writes go to a different file). Contains both UI-managed fields and runtime fields the UI doesn't expose.

KeyTypeDefaultValidationSurface
enabledbooleanfalseboolTelegram → Enabled
botTokenstring""string (plain)Telegram → Bot tokenstored in plain text
batchingDelayMsnumber2500>= 0Telegram → Batching delay
adminUserIdsnumber[][]not exposed via UINumeric Telegram IDs that receive Health Monitor notifications. Edit by hand.
pollingModebooleantruenot exposed via UItrue = long-poll (default). false = expect webhookUrl to be set.
webhookUrlstring""not exposed via UIPublic HTTPS URL for webhook mode. Ignored when pollingMode: true.

Default telegram.json

json
{
  "enabled": false,
  "botToken": "",
  "adminUserIds": [],
  "pollingMode": true,
  "webhookUrl": "",
  "batchingDelayMs": 2500
}

Bot token is plain text

The bot token is not encrypted at rest. Protect /data/config/telegram.json with normal filesystem permissions and back up the axiom-data volume to a trusted location. Do not commit or share this file.

The Telegram user directory (approval / assignment / status badges) is not in this file — it lives in the SQLite telegram_users table. See Telegram → Telegram users.


providers.json

LLM provider catalog. UI-managed via the Providers page; the schema is documented here for reference.

KeyTypeNotes
providersProviderConfig[]Every configured provider. Created/edited via the UI.
activeProviderstring (provider id)Currently active provider for chat. Set by Agent → Provider.
activeModelstring (model id)Active model within activeProvider.
fallbackProviderstringProvider switched to by Health Monitor when active goes down.
fallbackModelstringModel within fallbackProvider.
_commentstringCosmetic — written by the template, ignored at runtime.

ProviderConfig

KeyTypeNotes
idstringStable, unique within the file. Used everywhere as providerId.
namestringDisplay name in the UI.
typestringWire protocol, e.g. "openai-completions", "anthropic-messages".
providerTypestringLogical class — "openai", "anthropic", "ollama", "openai-chatgpt", etc.
providerstringpi-ai provider key.
baseUrlstringAPI base URL.
apiKeystring (encrypted)Encrypted with ENCRYPTION_KEY (packages/core/src/encryption.ts). Use the UI to write.
defaultModelstringModel id used when activeModel is unset.
enabledModelsstring[]?Model ids the user has enabled for this provider.
degradedThresholdMsnumber?Latency threshold for healthy → degraded transitions in Health Monitor.
modelsProviderModelConfig[]?Per-model overrides — context window, max tokens, reasoning support, fixed temperature, custom cost.
status"connected" | "error" | "untested"Last-known result of an explicit "test connection" click.
modelStatusesRecord<modelId, status>Per-model variant of status.
authMethod"apiKey" | "oauth"Determines whether apiKey or oauthCredentials is used.
oauthCredentialsOAuthCredentialsStored? (encrypted){ refresh, access, expires, extra } — only present when authMethod === "oauth".

Encryption

apiKey, oauthCredentials.refresh, oauthCredentials.access, and oauthCredentials.extra are AES-256-GCM-encrypted at rest using ENCRYPTION_KEY (see Environment Variables → ENCRYPTION_KEY). Losing or rotating that key makes existing values undecryptable — re-enter them via the UI to recover.

The encrypted form looks like enc::<base64>isEncrypted() checks the prefix; loadProvidersDecrypted() returns the plaintext form for runtime use; loadProvidersMasked() returns sk-••••••1234 for UI display.


secrets.json

Generic key/value secret store the agent and skills can read at runtime. UI-managed via the Secrets panel.

json
{
  "env": {
    "GH_TOKEN":            "enc::AAAA…",
    "MY_CUSTOM_API_TOKEN": "enc::BBBB…"
  }
}
  • File mode: 0600 (chowned to the container user).
  • Every value is AES-256-GCM-encrypted with ENCRYPTION_KEY.
  • Keys must match ^[A-Z][A-Z0-9_]*$ — uppercase letters, digits, underscores; must start with a letter.
  • Values are write-only from the API's perspective: existing entries are never returned in full. The UI lists sk-••••••1234-style masks via loadSecretsMasked().
  • Read at runtime by loadSecretsDecrypted() and surfaced to skill processes through environment variables matching the key name.

Don't bypass the UI for secrets.json

If you write this file by hand, you must encrypt the values yourself with the same ENCRYPTION_KEY the runtime uses. Practical advice: always use the Settings UI for secrets.

See Secrets → Where they end up for the full story.


skills.json

Catalog of installed skills, edited via the Skills page.

json
{
  "skills": [
    {
      "id": "github.com/owner/repo:main:path/to/skill",
      "owner": "owner",
      "name": "skill-name",
      "description": "What it does",
      "source": "github",
      "sourceUrl": "https://github.com/owner/repo",
      "path": "/data/skills/<id>",
      "enabled": true,
      "envKeys": ["MY_VAR"],
      "envValues": { "MY_VAR": "enc::…" },
      "emoji": "🛠",
      "installedAt": "2025-04-29T12:00:00Z"
    }
  ]
}
KeyTypeNotes
idstringStable id used as folder name under /data/skills/.
ownerstringRepository owner (or "local" for uploaded skills).
namestringDisplay name.
descriptionstringShort description shown in skill picker.
source"openclaw" | "github" | "upload"How the skill was installed.
sourceUrlstringOrigin URL (GitHub repo, openclaw entry, or empty for uploads).
pathstringAbsolute path to the installed skill directory.
enabledbooleanWhen false, the skill is loaded into the registry but not surfaced to the agent.
envKeysstring[]Names of env vars the skill expects.
envValuesRecord<string, string> (encrypted)Same encryption as secrets.json. UI presents these as masked password fields.
emojistring?Optional UI icon.
installedAtISO timestampSet on install / re-install.

Built-in agent skills are kept under /data/skills_agent/ and are not listed here — see File Paths.


How the UI writes files

PUT /api/settings   { … partial update … }


withLegacySettingsPayloadCompatibility   ← migrate old key shapes (heartbeat.intervalMinutes → top-level)


mergeHealthMonitor / mergeConsolidation / mergeFactExtraction /
mergeAgentHeartbeat / mergeTasks / mergeTts / mergeStt / mergeUploads
        │   ← per-field validators (validatePositiveNumber, validateEnum, validateHour, …)

fs.writeFileSync(/data/config/settings.json, …)
fs.writeFileSync(/data/config/telegram.json, …)


on*Changed hooks ── refresh in-memory caches & restart workers as needed


mapSettingsResponse → JSON returned to the UI

Source files:

  • packages/web-backend/src/api/modules/settings/service.tscreateSettingsService() orchestrates read / merge / write.
  • packages/web-backend/src/api/modules/settings/schema.ts — every per-field validator + merge function.
  • packages/web-backend/src/api/modules/settings/mapper.ts — shapes the response with all defaults filled in.
  • packages/core/src/contracts/settings.ts — single source of truth for types, enums (SETTINGS_THINKING_LEVELS, SETTINGS_TTS_PROVIDERS, …), and normalizeSettingsContract().
  • packages/core/src/config.tsensureConfigTemplates(), loadConfig(), getConfigDir().

Validators reference

ValidatorRule
validatePositiveNumberfinite number >= 1
validateNonNegativeNumberfinite number >= 0
validateIntegerRange(min, max)integer in [min, max]
validateHourinteger 0–23
validateNonEmptyStringstring with non-whitespace content
validateEnum(allowed)exact match against an enum array exported from the contract

A failed validator turns into a SettingsValidationError, which the route handler maps to HTTP 400 with the error message from the validator (e.g. "tasks.maxDurationMinutes must be a positive number"). No partial write happens — the file on disk stays unchanged.

Live-reload triggers

After a successful write, the service fires whichever of these hooks the changed fields imply:

TriggerRestarts / refreshes
sessionTimeoutMinutes changedagentCore.getSessionManager().setTimeoutMinutes(…)
language or timezone changedagentCore.refreshSystemPrompt()
thinkingLevel changedagentCore.setThinkingLevel(…)
healthMonitorIntervalMinutes or any healthMonitor.* changedonHealthMonitorSettingsChanged()
memoryConsolidation.* changedonConsolidationSettingsChanged()
agentHeartbeat.* changedonAgentHeartbeatSettingsChanged()
telegram.enabled or telegram.botToken changedonTelegramSettingsChanged() — restart bot

No container restart is needed for any of the above. Changes to fields outside this list (e.g. tasks.*, tts.*, stt.*, factExtraction.*) are read on next use rather than via a hook.


Migration & legacy fields

Things that may exist in older settings.json files and how the runtime handles them.

Old fieldReplaced byBehavior
heartbeat (whole block)healthMonitor + healthMonitorIntervalMinutesTemplate still seeds it on first boot. Live code never reads it. Replaced on first save.
healthMonitor.intervalMinutestop-level healthMonitorIntervalMinutesPUT migrates it via withLegacySettingsPayloadCompatibility.
tasks.statusUpdateIntervalMinutes (flat)tasks.statusUpdates.intervalMinutesMigrated on first save; tasks.statusUpdates.enabled stays false until opted in.
uploadRetentionDays (top-level)uploads.retentionDaysRead at runtime by getUploadRetentionDays() if the new key is absent. Replaced on first save of the Uploads panel.
batchingDelayMs (top-level in settings.json)telegram.batchingDelayMs (in telegram.json)Read at boot by the Telegram bot if telegram.json does not yet have the key. Replaced on first save of the Telegram panel.
braveSearchApiKey (top-level)builtinTools.webSearch.braveSearchApiKeyFolded in at boot if the new key is empty.
searxngUrl (top-level)builtinTools.webSearch.searxngUrlFolded in at boot if the new key is empty.

There is no automatic rewrite of these legacy fields. The new shape lands the next time the user saves the relevant panel; until then both shapes coexist and the runtime prefers the new one if both are present.


See also

Released under the MIT License.