Configuration Reference
At startup, zhin runtime start checks these project-root candidates in order. If more than one exists, startup fails instead of guessing:
config.ymlconfig.yamlconfig.jsonzhin.config.ymlzhin.config.yamlzhin.config.json
It is recommended to use zhin.config.yml consistently. All examples in this document use it.
For field-by-field types, required fields, defaults, and plugin schemas, use the generated configuration field reference. It is generated from Runtime source and plugin schema.json files, with drift blocked in CI.
How to use this reference
Run npx zhin setup to generate a bootable configuration, then change fields with this page. Run npx zhin doctor before committing. After startup, trust the generation shown in Console rather than treating a disk file as applied state.
| What you need to change | Where it belongs |
|---|---|
| Installed plugins and Features | dependencies and package.json#zhin |
| Host, plugin-instance, and AI values | zhin.config.yml |
| Secrets and environment differences | .env / .env.<environment>, referenced with ${VAR} |
| Workrooms, members, and chat or repository bindings | persistent Workroom Catalog, managed in Console |
| What the runtime actually published | Endpoint, capability catalog, and generation state in Console |
Configuration supplies values; it does not mount code. Install the dependency and update package.json#zhin before adding configuration for a new adapter, Feature, or plugin.
Loading and Validation Flow
Configuration passes JSON Schema validation, and only the keys listed below are allowed at the top level. A misspelled key produces Invalid Plugin config in zhin.config.yml at startup.
Top-level keys other than plugin and plugins are consumed by the CLI Host assembly layer. They are not passed to plugins.
Environment Variable Expansion
${VAR}, ${VAR:-default}, and ${VAR:=default} within string values are expanded:
http:
token: ${HTTP_TOKEN:-dev-token} # Falls back to dev-token when HTTP_TOKEN is not setAn unset variable without a default expands to an empty string. For fields such as apiKey, this triggers AI-provider soft pruning.
Dotenv loads .env then .env.<environment>. --environment selects the name and defaults to development. Keep secrets in environment variables, never in the configuration file.
Top-Level Keys
| Key | Type | Description |
|---|---|---|
log_level | string | number | Log level (e.g., info, debug); ZHIN_LOG_LEVEL / LOG_LEVEL environment variables can temporarily override |
http | object | HTTP Host (Console / API entry point) |
database | object | Database |
speech | object | Speech STT/TTS (requires @zhin.js/speech installed) |
assistant | object | Assistant Runtime (scheduled tasks / event API) |
ai | object | AI stack (requires @zhin.js/agent etc., see below) |
mcp | object | MCP Host (exposes bot tools as an MCP Server) |
a2a | object | A2A Host (Agent Card / remote Agent interop) |
htmlRenderer | object | Shotium HTML rendering (e.g., width / viewport / scale) |
plugin | object | Root Plugin (the application itself) configuration |
plugins | object | Child plugin configuration, keyed by instanceKey |
http
http:
port: 8086 # Runtime fallback when no config exists
host: 127.0.0.1 # Default 127.0.0.1
token: ${HTTP_TOKEN} # API Bearer token
corsOrigins: # Allowed cross-origin sources
- "https://console.zhin.dev"
base: /api # API mount pathWith no configuration, Runtime falls back to 8086; the current project scaffold writes 8068. Trust the project configuration and startup log.
Local development can access without token; production must set one. tokens supports multiple scoped tokens.
database
database:
dialect: sqlite # Default sqlite
filename: ./data/bot.dbdialectoptions:sqlite,mysql,pg,mongodb,redis,memory.- Fields other than
dialectare passed as-is to the corresponding dialect (e.g., pg/mysql connection parameters). - When
databaseis omitted entirely, it defaults to<project-root>/.zhin/data.sqlite.
speech
speech:
stt: { provider: ollama, model: whisper, host: http://localhost:11434 }
tts: { provider: edge, voice: zh-CN-XiaoxiaoNeural }When configured, this enables the speech pipeline and exposes voice_stt / voice_tts tools to the Agent. @zhin.js/speech is an optional peer dependency and must be installed separately.
assistant
assistant:
enabled: true # Enable unified JobStore, default false
profile:
enabled: true
file: assistant.profile.yml # Profile file, drives scheduled tasks
events:
enabled: true # Open POST /api/assistant/events
rateLimitPerMinute: 60 # Default 60
defaults:
notifyOnFailure: false # Whether to notify on Job failure, default falseThe remaining sub-key is queue (Schedule execution concurrency/retry/timeout). Schedule facts are always persisted to schedule-jobs.json; manage them with zhin schedule.
mcp / a2a
mcp:
enabled: true
path: /mcp
token: ${HTTP_TOKEN} # Falls back to http.token if unset
allowUnauthenticatedLocalhost: false # Recommend false for production
a2a:
enabled: true
path: /a2aBoth sections are mounted on the HTTP Host and enabled as needed; when not configured, the corresponding SDK is not loaded. Remote Workroom execution uses fixed-generation a2a.workroomCallbacks and a2a.workroomRemoteExecutors transport bindings. Project membership and conversation bindings remain in the persistent Workroom Catalog, not in process configuration. See the complete @zhin.js/a2a example on GitHub.
ai
The ai section is consumed by the Agent Host. Whenever ai / assistant is configured in any section, the startup graph loads the Agent stack; providers missing credentials are soft-pruned and do not block startup.
providers: Named Model Providers
ai:
providers:
deepseek:
sdk: deepseek # Required
apiKey: ${DEEP_SEEK_API_KEY}
openrouter:
sdk: openai-compatible
baseUrl: ${OPENROUTER_BASE_URL}
apiKey: ${OPENROUTER_API_KEY}
contextWindow: 32768
models: # Allowed model whitelist
- "openrouter/free"
ollama-local:
sdk: ollama
host: http://127.0.0.1:11434 # ollama uses host instead of baseUrl
cloudflare:
sdk: openai-compatible
apiKey: ${CLOUDFLARE_API_TOKEN}
accountId: ${CLOUDFLARE_ACCOUNT_ID}| Field | Description |
|---|---|
sdk | Required, one of six: openai, anthropic, google, deepseek, ollama, openai-compatible |
apiKey / baseUrl / host | Credentials and endpoint; ollama uses host, Cloudflare also requires accountId |
models | Explicit model list |
defaultModel, contextWindow, timeout, maxRetries, headers | Behavior tuning |
authScheme | Authorization header prefix, default 'Bearer ' |
imageGeneration | Text-to-image defaults (for drivers that support this capability, e.g., defaultModel, defaultSize, watermarkEnabled, promptSuffix) |
Provider aliases can be named arbitrarily; when sdk is not explicitly specified, it is inferred from the alias prefix (e.g., deepseek-main -> deepseek); if inference fails, it falls back to openai-compatible. The corresponding @ai-sdk/* packages are optional peers and should be installed based on which SDK is used.
agents: Bind Models and Roles
ai:
agents:
zhin: # Main Agent, default entry point
provider: openrouter # References the provider alias
model: openrouter/free
mcpServers: [icqq] # References names in ai.mcpServers
researcher:
provider: openrouter
model: openrouter/free
nickname: 'Researcher' # Agent self-reference and UI display name| Field | Description |
|---|---|
provider / model | Required, the bound provider alias and model |
nickname | Agent self-reference and UI display name |
mcpServers | List of MCP Server names visible to this Agent |
priority / match | Inbound routing: match can match by adapter / endpoint / scene / sceneId / hasMedia / contentContains, single or array |
permission.task | spawn_task visible child Agent types (glob -> allow / deny) |
mcpServers: External MCP Servers
ai:
mcpServers:
- name: icqq
transport: streamable-http # stdio | streamable-http | sse
url: ${ICQQ_MCP_URL}
headers:
Authorization: Bearer ${ICQQ_MCP_TOKEN}
- name: local-tools
transport: stdio
command: npx
args: ["-y", "some-mcp-server"]
env: { FOO: bar }Once connected successfully, these Servers' tools enter the Agent tool pool (assigned by agents.<name>.mcpServers).
Other ai Sub-sections
ai:
sessions: # Session storage
maxHistory: 200 # Default 200 in database mode, 100 in memory mode
expireMs: 604800000 # Default 7 days in database mode, 24h in memory mode
useDatabase: true # Default true; uses Database Root Host (.zhin/data.sqlite when database is omitted)
context: # Group chat context recording
enabled: true
maxRecentMessages: 100
summaryThreshold: 50
keepAfterSummary: 10
maxContextTokens: 4000
memory: # Three-layer Markdown file memory, enabled by default
semantic:
enabled: true # Generation-owned memory_search/memory_upsert; requires the Database Host
autoConsolidate: false
# memory_entries must be ready during candidate activation or that generation fails closed
# memoryMcp is deprecated; use the native memory_search / memory_upsert tools above
access: # AI access control
mode: open # open | closed | whitelist, default open
users: [] # Allowed user IDs in whitelist mode
groups: []
denyMessage: ...
agent: # Tool execution security
inboundQueue:
groupMode: supersede # supersede (default) | fifo
execSecurity: allowlist # deny | allowlist | full
execPreset: readonly # readonly | network | development | custom
execAllowlist: ["^ls ", "^cat "]
maxIterations: 15 # Maximum tool iterations per turn, default 15
thinkingPreview: false # Show LLM actual thinking content (truncated) in activity feedback instead of static "Thinking...", default false
thinkingPreviewMaxLength: 200 # Max chars for thinkingPreview, default 200
trigger: # AI trigger rules
prefixes: ["ai:"] # Trigger prefixes, default ['#', 'AI:', 'ai:']
respondToAt: true # Respond to @bot, default true
respondToPrivate: true # Private chats bypass prefix requirement, default true
ignorePrefixes: ['/', '!', '!'] # Avoid conflicting with commands
timeout: 60000ai.multimodal governs multimodal input and output. ai.knowledge.baseDir selects the local knowledge directory and defaults to knowledge.
Remote Agents no longer attach through ai.remoteAgents. An optional A2A Executor enters through the persistent Workroom Catalog and generation-owned authority, under Assignment lease/fence and Journal contracts.
ai.workrooms has been removed. Projects, members, and collaboration spaces are managed by the persistent Workroom Catalog in Console; revision-checked saves take effect without restarting the runtime.
plugin and plugins
# Root Plugin (the application itself) configuration; keys are determined by the application's config schema
plugin:
terminal:
interactive: true
# Child plugin configuration, keyed by instanceKey
plugins:
sandbox:
endpoints:
- context: sandbox
name: sandbox-bot
owner: sandbox-userinstanceKey defaults to the last package-name segment without the adapter-, plugin-, or service- prefix. For example, @zhin.js/adapter-icqq becomes icqq.
zhin install writes plugins.<instanceKey> and mounts the package under package.json#zhin.plugins.
Adapter Instances: master / trusted / commandPrefix
Platform adapter plugin instance configurations support three common keys:
| Key | Location | Description |
|---|---|---|
master | Instance top-level | Stable Endpoint Owner ID used for authorization and explicit /approve management commands |
trusted | Instance top-level / per-endpoint | Trusted user ID list; array or whitespace/comma-separated string |
commandPrefix | Instance top-level / per-endpoint | Command prefix, default ''; per-endpoint values override top-level |
endpoints Array: One Instance with Multiple Accounts
Multi-account adapters use the endpoints array to declare multiple Endpoints:
plugins:
icqq:
master: "${ICQQ_MASTER}" # Top-level shared: applies to all endpoints
endpoints: # Per-entry: name is required, other fields merge with top-level (per-entry takes priority)
- name: "${ICQQ_ACCOUNT_1}"
- name: "${ICQQ_ACCOUNT_2}"Expansion rules (@zhin.js/adapter's expandEndpointConfigs):
- Each endpoint's final configuration = instance configuration (without the
endpointskey) shallow-merged with the entry's fields, per-entry fields override top-level. nameis required and must be a non-empty string; must not contain~; duplicates keep only the first with a warning.- When
endpointsis empty or all entries are invalid, it falls back to a single Endpoint (named after the instanceKey).
Real-world example (QQ official bot, main account through proxy gateway + sandbox account coexisting):
plugins:
qq:
endpoints:
- name: zhin
appid: "${QQ_APPID}"
secret: "${QQ_SECRET}"
mode: websocket
sandbox: false
gatewayUrl: "https://bots.example.com/gateway/102005927"
intents: [GUILDS, GROUP_AND_C2C_EVENT, PUBLIC_GUILD_MESSAGES]
- name: "102069707"
appid: "${QQ_SANDBOX_APPID}"
secret: "${QQ_SANDBOX_SECRET}"
mode: websocket
sandbox: true
intents: [GUILDS, GROUP_AND_C2C_EVENT]
slack:
endpoints:
- name: zhin
token: "${SLACK_TOKEN}"
appToken: "${SLACK_APP_TOKEN}"
signingSecret: "${SLACK_SIGNING_SECRET}"
socketMode: trueFor adapter-specific fields (appid, intents, socketMode, url, access_token, etc.), see the README of the corresponding adapter package.
Minimal Configuration
You can run without any Host sections -- this is the entire configuration for examples/minimal-bot:
plugin:
terminal:
interactive: true
plugins: {}After starting, use the zhin setup wizard to progressively add database / adapters / AI (see CLI Reference); for how to run, see zhin runtime start in Detail.