Configuration

April 1, 2026 · View on GitHub

Complete reference for config.toml. Spacebot looks for this file at ~/.spacebot/config.toml (or $SPACEBOT_DIR/config.toml). If no config file exists, it falls back to environment variables.

File Location

~/.spacebot/config.toml          # default
$SPACEBOT_DIR/config.toml        # env override
spacebot --config /path/to.toml  # CLI override

Environment Variables

These environment variables control instance-level behavior and are not set in config.toml.

VariableDefaultDescription
SPACEBOT_DIR~/.spacebotInstance root directory. All data, config, databases, and sockets live under this path. Set this to run multiple isolated instances (e.g. dev and prod) side by side.
SPACEBOT_DEPLOYMENTauto-detectedDeployment mode: docker, hosted, or native (auto-detected). Affects API bind address, agent limits, and update behavior.
SPACEBOT_MAX_AGENTSunlimitedMaximum number of agents (enforced in hosted mode only).
SPACEBOT_CRON_TIMEZONEserver localDefault timezone for cron active-hours evaluation (IANA name). Overridden by defaults.cron_timezone or agents.cron_timezone in config.
SPACEBOT_USER_TIMEZONEinherits cronDefault timezone for channel/worker temporal context. Overridden by config equivalents.
SPACEBOT_CHANNEL_MODELanthropic/claude-sonnet-4-20250514Default channel model (env-only mode).
SPACEBOT_WORKER_MODELanthropic/claude-haiku-4.5-20250514Default worker model (env-only mode).

Full Reference

# --- LLM Provider Credentials ---
# Instance-level, shared by all agents. At least one key or provider is required.
[llm]
anthropic_key = "env:ANTHROPIC_API_KEY"
openai_key = "env:OPENAI_API_KEY"
openrouter_key = "env:OPENROUTER_API_KEY"
kilo_key = "env:KILO_API_KEY"
zhipu_key = "env:ZHIPU_API_KEY"
groq_key = "env:GROQ_API_KEY"
together_key = "env:TOGETHER_API_KEY"
fireworks_key = "env:FIREWORKS_API_KEY"
deepseek_key = "env:DEEPSEEK_API_KEY"
xai_key = "env:XAI_API_KEY"
mistral_key = "env:MISTRAL_API_KEY"
opencode_zen_key = "env:OPENCODE_ZEN_API_KEY"
opencode_go_key = "env:OPENCODE_GO_API_KEY"

# Custom LLM providers (alternative to legacy keys)
[llm.provider.my_anthropic]
api_type = "anthropic"
base_url = "https://api.anthropic.com"
api_key = "env:MY_ANTHROPIC_KEY"
name = "My Custom Anthropic"

[llm.provider.my_openai]
api_type = "openai_responses"
base_url = "https://api.openai.com"
api_key = "env:MY_OPENAI_KEY"

[llm.provider.local_openai]
api_type = "openai_completions"
base_url = "http://localhost:8080" # do not include /v1; Spacebot appends endpoint paths
api_key = "env:LOCAL_OPENAI_KEY"
name = "Local OpenAI Compatible"

# --- Instance Defaults ---
# All agents inherit these. Individual agents can override any field.
[defaults]
max_concurrent_branches = 5    # max branches per channel
max_turns = 5                  # max LLM turns per channel message
context_window = 128000        # context window size in tokens
history_backfill_count = 50    # messages to fetch from platform on new channel
worker_log_mode = "errors_only" # "errors_only", "all_separate", or "all_combined"
cron_timezone = "UTC"          # optional default timezone for cron active hours
user_timezone = "UTC"          # optional default timezone for channel/worker time context

# Model routing per process type.
[defaults.routing]
channel = "anthropic/claude-sonnet-4-20250514"
branch = "anthropic/claude-sonnet-4-20250514"
worker = "anthropic/claude-haiku-4.5-20250514"
compactor = "anthropic/claude-haiku-4.5-20250514"
cortex = "anthropic/claude-haiku-4.5-20250514"
rate_limit_cooldown_secs = 60

# Task-type overrides for workers/branches.
[defaults.routing.task_overrides]
coding = "anthropic/claude-sonnet-4-20250514"

# Fallback chains when a model is rate-limited or down.
[defaults.routing.fallbacks]
"anthropic/claude-sonnet-4-20250514" = ["anthropic/claude-haiku-4.5-20250514"]

# Context compaction thresholds (fraction of context_window).
[defaults.compaction]
background_threshold = 0.80    # background summarization
aggressive_threshold = 0.85    # aggressive summarization
emergency_threshold = 0.95     # drop oldest 50%, no LLM

# Cortex (system observer) settings.
[defaults.cortex]
tick_interval_secs = 30
worker_timeout_secs = 600
branch_timeout_secs = 60
circuit_breaker_threshold = 3  # consecutive failures before auto-disable

# Warmup controls for cold-start behavior and manual rewarm.
[defaults.warmup]
enabled = true
eager_embedding_load = true
refresh_secs = 900
startup_delay_secs = 5

# Browser automation for workers.
[defaults.browser]
enabled = true
headless = true
evaluate_enabled = false
persist_session = false                  # keep browser alive across worker lifetimes
close_policy = "close_browser"           # "close_browser", "close_tabs", or "detach"
executable_path = "/path/to/chrome"      # optional, auto-detected
screenshot_dir = "/path/to/screenshots"  # optional, defaults to data_dir/screenshots

# --- Agents ---
# At least one agent is required. First agent or the one with default = true
# is the default.
[[agents]]
id = "main"
default = true
workspace = "/custom/workspace/path"   # optional, defaults to ~/.spacebot/agents/{id}/workspace
cron_timezone = "America/Los_Angeles"  # optional per-agent cron timezone override
user_timezone = "America/Los_Angeles"  # optional per-agent timezone override for channel/worker time context

# Per-agent routing overrides (merges with defaults).
[agents.routing]
channel = "anthropic/claude-opus-4-20250514"

# Per-agent sandbox configuration.
[agents.sandbox]
mode = "enabled"                               # "enabled" (default) or "disabled"
writable_paths = ["/home/user/projects/myapp"] # additional writable directories

# Per-agent cron jobs.
[[agents.cron]]
id = "daily-check"
prompt = "Check in on ongoing projects and report status."
cron_expr = "0 9 * * *"
delivery_target = "discord:123456789"
active_start_hour = 9
active_end_hour = 17
enabled = true

# --- Messaging Platforms ---
[messaging.discord]
enabled = true
token = "env:DISCORD_BOT_TOKEN"
dm_allowed_users = ["user_id_1", "user_id_2"]

[messaging.telegram]
enabled = true
token = "env:TELEGRAM_BOT_TOKEN"
dm_allowed_users = ["user_id_1"]

[messaging.webhook]
enabled = true
port = 18789
bind = "127.0.0.1"

# --- Bindings ---
# Routes platform conversations to agents. First match wins.
[[bindings]]
agent_id = "main"
channel = "discord"
guild_id = "123456789"
channel_ids = ["456", "789"]   # optional, restricts to specific channels

[[bindings]]
agent_id = "main"
channel = "webhook"

Value References

Any string value in the config supports three resolution modes:

PrefixResolutionExample
secret:Look up from the secret store"secret:ANTHROPIC_API_KEY"
env:Read from system environment variable"env:ANTHROPIC_API_KEY"
(none)Literal value"sk-ant-..."
# From the secret store (recommended)
anthropic_key = "secret:ANTHROPIC_API_KEY"

# From an environment variable
openai_key = "env:OPENAI_API_KEY"

# Literal value (not recommended — use secret: or env: instead)
groq_key = "gsk_abc123..."

The secret: prefix resolves from the agent's secret store at config load time. If the secret doesn't exist, the value is treated as missing and implicit env fallbacks are tried.

LLM keys also have implicit env fallbacks — if no key is set in the TOML, Spacebot checks ANTHROPIC_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, KILO_API_KEY, and OPENCODE_GO_API_KEY automatically.

Use POST /api/secrets/migrate to automatically move plaintext credentials from config.toml into the secret store and replace them with secret: references. See Secret Store -- Migration for details.

Env-Only Mode

If no config.toml exists, Spacebot runs from environment variables alone:

export ANTHROPIC_API_KEY="sk-ant-..."
# optional model overrides
export SPACEBOT_CHANNEL_MODEL="anthropic/claude-sonnet-4-20250514"
export SPACEBOT_WORKER_MODEL="anthropic/claude-haiku-4.5-20250514"
spacebot

This creates a single "main" agent with default settings.

Config Resolution Order

For any setting, the resolution chain is:

agent-level override  >  [defaults] section  >  env var fallback (if supported)  >  hardcoded default

An agent with no overrides inherits everything from [defaults]. An agent with partial overrides gets those values from its own config and everything else from defaults. See Agents for how agent config merging works.

Model Names

Model names include the provider as a prefix:

ProviderFormatExample
Anthropicanthropic/<model>anthropic/claude-sonnet-4-20250514
OpenAIopenai/<model>openai/gpt-4o
OpenRouteropenrouter/<provider>/<model>openrouter/anthropic/claude-sonnet-4-20250514
Kilo Gatewaykilo/<provider>/<model>kilo/anthropic/claude-sonnet-4.5
Custom provider<provider_id>/<model>my_openai/gpt-4o-mini

You can mix providers across process types. See Routing for the full routing system.

Hot Reload

Most config values are hot-reloaded when their files change. Spacebot watches config.toml, identity files, and skill directories. Changes are debounced to 2 seconds and applied to all running channels, workers, and branches without restart.

What Hot-Reloads

SettingReloads?Scope
Model routingYesNext LLM call uses the new model
Compaction thresholdsYesNext compaction check uses new thresholds
max_turnsYesNext channel message uses new limit
context_windowYesNext compaction/worker check uses new size
max_concurrent_branchesYesNext branch spawn checks new limit
Browser configYesNext worker spawn uses new config
Warmup configYesNext warmup pass uses new values
Identity files (SOUL.md, etc.)YesNext channel message renders new identity
Skills (SKILL.md files)YesNext message / worker spawn sees new skills
BindingsYesNext message routes using new bindings
Discord/Slack permissionsYesNext message checks new permission rules

What Needs Restart

SettingWhy
LLM API keysProvider clients are initialized once (applies to secret:, env:, and literal values)
Messaging adapters (Discord token, webhook bind/port)Adapter connections are long-lived
Agent topology (adding/removing [[agents]])Databases and event buses are per-agent
Database pathsConnections are opened once at startup
System promptsCompiled into the binary via include_str!

How It Works

A file watcher (via the notify crate) monitors:

  • ~/.spacebot/config.toml
  • ~/.spacebot/skills/ (instance-level skills)
  • Each agent's root directory (identity files: SOUL.md, IDENTITY.md, ROLE.md)
  • Each agent's workspace/skills/ (workspace-level skills)

On file change, Spacebot re-reads the changed files and atomically swaps the new values into the live RuntimeConfig using arc-swap. All consumers (channels, branches, workers, compactors, cron jobs) read from RuntimeConfig on every use, so they pick up changes immediately.

File change detected
  → debounce 2 seconds (collapses rapid edits)
  → categorize: config / identity / skills
  → re-parse changed files
  → ArcSwap::store() on RuntimeConfig fields
  → all running processes see new values on next read

No lock contention. Reads are wait-free via arc-swap. The watcher runs on a dedicated thread; reloads don't block the async runtime.

System Prompts

System prompts (channel, branch, worker, compactor, cortex, etc.) are Jinja2 templates embedded in the binary at compile time via include_str!. They live in the source tree at prompts/en/*.md.j2 and are not user-editable at runtime. Changing prompts requires rebuilding the binary.

On-Disk Layout

~/.spacebot/
├── config.toml                    # main config (hot-reloaded)
├── embedding_cache/               # shared embedding model cache
├── skills/                        # instance-level skills (hot-reloaded)
│   └── weather/
│       └── SKILL.md
└── agents/
    └── main/
        ├── SOUL.md                # personality (hot-reloaded)
        ├── IDENTITY.md            # name and nature (hot-reloaded)
        ├── ROLE.md                # responsibilities, scope (hot-reloaded)
        ├── workspace/             # sandbox boundary for worker file tools
        │   ├── skills/            # workspace-level skills (hot-reloaded)
        │   └── ingest/            # drop files here for memory ingestion
        ├── data/
        │   ├── spacebot.db        # SQLite
        │   ├── lancedb/           # vector search
        │   ├── config.redb        # key-value settings
        │   ├── settings.redb      # runtime settings (worker_log_mode, etc.)
        │   ├── secrets.redb       # secret store (categories, encryption)
        │   └── logs/              # worker execution logs
        └── archives/              # compaction transcripts

Sections Reference

Migration from Legacy Keys

Legacy keys (anthropic_key, openai_key, etc.) are still supported and automatically converted to provider entries internally. For example:

Legacy format:

[llm]
anthropic_key = "env:ANTHROPIC_API_KEY"
openai_key = "env:OPENAI_API_KEY"

Internal representation (auto-created):

[llm.provider.anthropic]
api_type = "anthropic"
base_url = "https://api.anthropic.com"
api_key = "env:ANTHROPIC_API_KEY"

[llm.provider.openai]
api_type = "openai_completions"
base_url = "https://api.openai.com"
api_key = "env:OPENAI_API_KEY"

If you define a custom provider with the same ID as a legacy key, your custom configuration takes precedence.

Legacy Keys

KeyTypeDefaultDescription
anthropic_keystringNoneAnthropic API key (secret:NAME, env:VAR_NAME, or literal)
openai_keystringNoneOpenAI API key (secret:NAME, env:VAR_NAME, or literal)
openrouter_keystringNoneOpenRouter API key (secret:NAME, env:VAR_NAME, or literal)
kilo_keystringNoneKilo Gateway API key (secret:NAME, env:VAR_NAME, or literal)
zhipu_keystringNoneZhipu AI (GLM) API key (secret:NAME, env:VAR_NAME, or literal)
groq_keystringNoneGroq API key (secret:NAME, env:VAR_NAME, or literal)
together_keystringNoneTogether AI API key (secret:NAME, env:VAR_NAME, or literal)
fireworks_keystringNoneFireworks AI API key (secret:NAME, env:VAR_NAME, or literal)
deepseek_keystringNoneDeepSeek API key (secret:NAME, env:VAR_NAME, or literal)
xai_keystringNoneXAI API key (secret:NAME, env:VAR_NAME, or literal)
mistral_keystringNoneMistral API key (secret:NAME, env:VAR_NAME, or literal)
opencode_zen_keystringNoneOpenCode Zen API key (secret:NAME, env:VAR_NAME, or literal)
opencode_go_keystringNoneOpenCode Go API key (secret:NAME, env:VAR_NAME, or literal)
gemini_keystringNoneGemini API key (secret:NAME, env:VAR_NAME, or literal)
nvidia_keystringNoneNVIDIA API key (secret:NAME, env:VAR_NAME, or literal)
minimax_keystringNoneMiniMax API key (secret:NAME, env:VAR_NAME, or literal)
moonshot_keystringNoneMoonshot API key (secret:NAME, env:VAR_NAME, or literal)
github_copilot_keystringNoneGitHub Copilot PAT (secret:NAME, env:VAR_NAME, or literal)

Custom Providers

Custom providers allow configuring LLM providers with custom endpoints and API types. Use either legacy keys or custom providers.

[llm.provider.<id>]
api_type = "anthropic"          # Required - see supported values below
base_url = "https://api..."     # Required - valid URL
api_key = "env:API_KEY"         # Required - API key (supports env:VAR_NAME format)
name = "My Provider"            # Optional - friendly name for display
FieldTypeRequiredDescription
api_typestringYesAPI protocol type. One of: anthropic, openai_completions, openai_chat_completions, openai_responses, gemini, kilo_gateway, or azure
base_urlstringYesBase URL of the API endpoint. Must be a valid URL (including protocol). For Azure, must end with .openai.azure.com
api_keystringYesAPI key for authentication. Supports secret:NAME and env:VAR_NAME syntax
namestringNoOptional friendly name for the provider (displayed in logs and UI)
api_versionstringAzure onlyAzure API version (format: YYYY-MM-DD or YYYY-MM-DD-preview)
deploymentstringAzure onlyAzure deployment name (alphanumeric, hyphens, and dots allowed)

Note:

  • For openai_completions, openai_chat_completions, and openai_responses, configure base_url as the provider root URL (usually without a trailing /v1).
  • Spacebot appends the endpoint path automatically:
    • openai_completions -> /v1/chat/completions
    • openai_chat_completions -> /chat/completions
    • openai_responses -> /v1/responses
    • kilo_gateway -> /chat/completions plus Kilo-required HTTP-Referer / X-Title headers
  • If you include /v1 in base_url, requests can end up with duplicated paths such as /v1/v1/....

Provider ID Requirements:

  • 1-64 characters long
  • Cannot contain / or whitespace
  • Case-insensitive (stored as lowercase)

Examples

Anthropic-compatible provider:

[llm.provider.custom_anthropic]
api_type = "anthropic"
base_url = "https://api.anthropic.com"
api_key = "env:CUSTOM_ANTHROPIC_KEY"
name = "Anthropic EU"

Azure OpenAI provider:

[llm.provider.azure]
api_type = "azure"
base_url = "https://my-resource.openai.azure.com"
api_key = "env:AZURE_API_KEY"
api_version = "2024-02-15"  # Required for Azure
deployment = "gpt-4o"        # Required for Azure (deployment name)
name = "Azure OpenAI"

Azure Requirements:

  • base_url must end with .openai.azure.com
  • api_version must match format: YYYY-MM-DD or YYYY-MM-DD-preview
  • deployment can contain alphanumeric characters, hyphens, and dots (e.g., gpt-4o, gpt-5.2)
  • Model names in routing should use the format: azure/<deployment-name>

OpenAI Completions provider:

[llm.provider.local_llm]
api_type = "openai_completions"
base_url = "http://localhost:8080" # no /v1 in base_url
api_key = "env:LOCAL_LLM_KEY"
name = "Local LLaMA Server"

At least one provider (legacy key or custom provider) must be configured.

[defaults]

KeyTypeDefaultDescription
max_concurrent_branchesinteger5Max branches per channel
max_turnsinteger5Max LLM turns per channel message
context_windowinteger128000Context window size in tokens
history_backfill_countinteger50Messages to fetch from platform on new channel
worker_log_modestring"errors_only"Worker log persistence: "errors_only", "all_separate", or "all_combined"
cron_timezonestringNoneDefault timezone for cron active-hours evaluation (IANA name like UTC or America/New_York)
user_timezonestringinherits cron_timezoneDefault timezone for channel/worker temporal context (IANA name)

[defaults.routing]

KeyTypeDefaultDescription
channelstringanthropic/claude-sonnet-4-20250514Model for user-facing channels
branchstringanthropic/claude-sonnet-4-20250514Model for thinking branches
workerstringanthropic/claude-haiku-4.5-20250514Model for task workers
compactorstringanthropic/claude-haiku-4.5-20250514Model for summarization
cortexstringanthropic/claude-haiku-4.5-20250514Model for system observation
rate_limit_cooldown_secsinteger60How long to deprioritize a rate-limited model

Routing selects providers by the prefix before the first / in the model name.

[defaults.routing]
channel = "my_openai/gpt-4o-mini"
worker = "custom_anthropic/claude-3-5-sonnet"

[llm.provider.my_openai]
api_type = "openai_completions"
base_url = "https://api.openai.com"
api_key = "env:OPENAI_API_KEY"

[llm.provider.custom_anthropic]
api_type = "anthropic"
base_url = "https://api.anthropic.com"
api_key = "env:ANTHROPIC_API_KEY"

If no prefix is provided (for example claude-sonnet-4-20250514), Spacebot defaults to the anthropic provider.

[defaults.routing.task_overrides]

Map of task type names to model names. Applied when workers or branches are spawned with a specific task type.

[defaults.routing.task_overrides]
coding = "anthropic/claude-sonnet-4-20250514"
deep_reasoning = "anthropic/claude-opus-4-20250514"

[defaults.routing.fallbacks]

Map of model names to ordered fallback chains. Used when the primary model returns a retriable error.

[defaults.routing.fallbacks]
"anthropic/claude-sonnet-4-20250514" = ["anthropic/claude-haiku-4.5-20250514"]

[defaults.compaction]

KeyTypeDefaultDescription
background_thresholdfloat0.80Start background summarization
aggressive_thresholdfloat0.85Start aggressive summarization
emergency_thresholdfloat0.95Emergency truncation (no LLM, drop oldest 50%)

Thresholds are fractions of context_window.

[defaults.cortex]

KeyTypeDefaultDescription
tick_interval_secsinteger30How often the cortex runtime loop runs maintenance ticks while continuously observing events
worker_timeout_secsinteger600Worker idle timeout before cancellation
branch_timeout_secsinteger60Branch timeout before cancellation
detached_worker_timeout_retry_limitinteger2Retry limit before quarantining detached workers to backlog
supervisor_kill_budget_per_tickinteger8Max number of overdue processes supervisor may cancel per health tick
circuit_breaker_thresholdinteger3Consecutive failures before auto-disable

[defaults.warmup]

KeyTypeDefaultDescription
enabledbooltrueEnable background warmup loop
eager_embedding_loadbooltrueWarm embedding model before first recall/write workload
refresh_secsinteger900Seconds between background warmup passes
startup_delay_secsinteger5Delay before first warmup pass after boot

When warmup is enabled, it is the primary bulletin refresh path. The cortex runtime loop still performs fallback bulletin/profile refresh when warmup is disabled or when the cached bulletin is stale (bulletin_age_secs >= max(1, warmup.refresh_secs)).

Dispatch readiness is derived from warmup runtime state:

  • warmup state must be warm
  • embedding must be ready
  • bulletin age must be fresh (<= max(60s, refresh_secs * 2))

When branch/worker/cron dispatch happens before readiness is satisfied, Spacebot still dispatches, increments cold-dispatch metrics, and queues a forced warmup pass in the background.

[defaults.browser]

KeyTypeDefaultDescription
enabledbooltrueWhether workers have browser tools
headlessbooltrueRun Chrome headless
evaluate_enabledboolfalseAllow JavaScript evaluation
persist_sessionboolfalseKeep browser alive across worker lifetimes. Tabs, cookies, and logins survive between tasks. Requires agent restart to take effect.
close_policystring"close_browser"What happens on close: "close_browser" (kill Chrome), "close_tabs" (close tabs, keep browser), "detach" (disconnect, leave everything)
executable_pathstringNoneCustom Chrome/Chromium path
screenshot_dirstringNoneDirectory for screenshots

[[agents]]

KeyTypeDefaultDescription
idstringrequiredAgent identifier
defaultboolfalseWhether this is the default agent
workspacestring~/.spacebot/agents/{id}/workspaceCustom workspace path
cron_timezonestringinheritsPer-agent timezone override for cron active-hours evaluation
user_timezonestringinheritsPer-agent timezone override for channel/worker temporal context
max_concurrent_branchesintegerinheritsOverride instance default
max_turnsintegerinheritsOverride instance default
context_windowintegerinheritsOverride instance default

Agent-specific routing is set via [agents.routing] with the same keys as [defaults.routing].

[agents.sandbox]

OS-level filesystem containment and environment sanitization for shell and exec tool subprocesses. Uses bubblewrap (Linux) or sandbox-exec (macOS) to enforce read-only access to everything outside the workspace. Environment sanitization runs in all modes -- workers never inherit the parent's environment variables.

See Sandbox for a full explanation of how containment, environment sanitization, leak detection, and durable binaries work.

KeyTypeDefaultDescription
modestring"enabled""enabled" for kernel-enforced containment, "disabled" for passthrough (full host filesystem access; env sanitization still applies)
writable_pathsstring[][]Additional directories the agent can write to beyond its workspace
passthrough_envstring[][]Environment variable names to forward from the parent process to worker subprocesses

When mode = "enabled", shell and exec commands run inside a mount namespace where the entire filesystem is read-only except:

  • The agent's workspace directory
  • /tmp (private per invocation)
  • /dev (standard device nodes)
  • Any paths listed in writable_paths

The agent's data directory (databases, config) is explicitly re-mounted read-only even if it would otherwise be writable due to path overlap.

Regardless of mode, all worker subprocesses start with a clean environment. Only PATH (with tools/bin prepended), safe variables (HOME, USER, LANG, TERM, TMPDIR), and any passthrough_env entries are injected. HOME is mode-dependent: workspace path when sandboxed, parent HOME when passthrough is enabled. Use passthrough_env with least privilege: only forward variables required by worker tools (for example specific credentials set in Docker Compose or systemd), and avoid forwarding broad or highly sensitive credentials.

If the sandbox backend isn't available (e.g. bubblewrap not installed), processes run unsandboxed with a warning at startup.

[agents.sandbox]
mode = "enabled"
writable_paths = ["/home/user/projects/myapp", "/var/data/shared"]
passthrough_env = ["GH_TOKEN", "GITHUB_TOKEN"]

[[agents.cron]]

KeyTypeDefaultDescription
idstringrequiredCron job identifier
promptstringrequiredPrompt sent to a fresh channel on each tick
cron_exprstringNoneStrict wall-clock schedule (cron expression, e.g. 0 9 * * *)
interval_secsinteger3600Seconds between firings
delivery_targetstringrequiredWhere to send results (adapter:target)
active_start_hourintegerNoneStart of active hours window (24h format)
active_end_hourintegerNoneEnd of active hours window
enabledbooltrueWhether this cron job is active

Cron timezone precedence is:

  1. agents.cron_timezone
  2. defaults.cron_timezone
  3. SPACEBOT_CRON_TIMEZONE
  4. server local timezone

If a configured timezone is invalid, Spacebot logs a warning and falls back to server local time.

Channel/worker temporal context timezone precedence is:

  1. agents.user_timezone
  2. defaults.user_timezone
  3. SPACEBOT_USER_TIMEZONE
  4. resolved cron timezone (from agents.cron_timezone / defaults.cron_timezone / SPACEBOT_CRON_TIMEZONE)
  5. server local timezone

[messaging.discord]

KeyTypeDefaultDescription
enabledboolfalseEnable Discord adapter
tokenstringNoneBot token (or env:VAR_NAME)
instancestable[][]Optional named Discord bot instances
dm_allowed_usersstring[][]User IDs allowed to DM the bot

[[messaging.discord.instances]]

KeyTypeDefaultDescription
namestringrequiredInstance selector used by bindings (adapter = "name")
enabledbooltrueEnable this named instance
tokenstringrequiredBot token (or env:VAR_NAME)
dm_allowed_usersstring[][]User IDs allowed to DM this instance
allow_bot_messagesboolfalseWhether this instance accepts bot-authored messages

[messaging.slack]

KeyTypeDefaultDescription
enabledboolfalseEnable Slack adapter
bot_tokenstringNoneBot token (or env:VAR_NAME)
app_tokenstringNoneApp-level token (or env:VAR_NAME)
instancestable[][]Optional named Slack app instances
dm_allowed_usersstring[][]Slack user IDs allowed to DM the bot

[[messaging.slack.instances]]

KeyTypeDefaultDescription
namestringrequiredInstance selector used by bindings (adapter = "name")
enabledbooltrueEnable this named instance
bot_tokenstringrequiredBot token (or env:VAR_NAME)
app_tokenstringrequiredApp-level token (or env:VAR_NAME)
dm_allowed_usersstring[][]Slack user IDs allowed to DM this instance

[messaging.telegram]

KeyTypeDefaultDescription
enabledboolfalseEnable Telegram adapter
tokenstringNoneBot token from @BotFather (or env:VAR_NAME). Falls back to TELEGRAM_BOT_TOKEN env var
instancestable[][]Optional named Telegram bot instances
dm_allowed_usersstring[][]User IDs allowed to DM the bot. Empty = DMs from anyone accepted

[[messaging.telegram.instances]]

KeyTypeDefaultDescription
namestringrequiredInstance selector used by bindings (adapter = "name")
enabledbooltrueEnable this named instance
tokenstringrequiredBot token (or env:VAR_NAME)
dm_allowed_usersstring[][]User IDs allowed to DM this instance

[messaging.twitch]

KeyTypeDefaultDescription
enabledboolfalseEnable Twitch adapter
usernamestringNoneBot login username
oauth_tokenstringNoneOAuth token (oauth:... or plain token)
instancestable[][]Optional named Twitch bot instances
channelsstring[][]Channels to join
trigger_prefixstringNoneOptional prefix required to trigger replies

[[messaging.twitch.instances]]

KeyTypeDefaultDescription
namestringrequiredInstance selector used by bindings (adapter = "name")
enabledbooltrueEnable this named instance
usernamestringrequiredBot login username
oauth_tokenstringrequiredOAuth token (oauth:... or plain token)
channelsstring[][]Channels to join for this instance
trigger_prefixstringNoneOptional prefix required to trigger replies

[messaging.email]

KeyTypeDefaultDescription
enabledboolfalseEnable Email adapter
imap_hoststringNoneIMAP host (or env:VAR_NAME)
imap_portinteger993IMAP port
imap_usernamestringNoneIMAP username (or env:VAR_NAME)
imap_passwordstringNoneIMAP password (or env:VAR_NAME)
imap_use_tlsbooltrueUse direct TLS for IMAP
smtp_hoststringNoneSMTP host (or env:VAR_NAME)
smtp_portinteger587SMTP port
smtp_usernamestringNoneSMTP username (or env:VAR_NAME)
smtp_passwordstringNoneSMTP password (or env:VAR_NAME)
smtp_use_starttlsbooltrueUse STARTTLS for SMTP
from_addressstringNoneSender address for outgoing replies (or env:VAR_NAME)
from_namestringNoneOptional sender display name
poll_interval_secsinteger30How often to check for new email
foldersstring[]["INBOX"]IMAP folders to poll
allowed_sendersstring[][]Optional allowlist for inbound senders (empty = all)
max_body_bytesinteger262144Max inbound body bytes before truncation
max_attachment_bytesinteger10485760Max attachment bytes to process metadata for

[[messaging.email.instances]]

KeyTypeDefaultDescription
namestringrequiredInstance selector used by bindings (adapter = "name")
enabledbooltrueEnable this named instance
imap_hoststringrequiredIMAP host (or env:VAR_NAME)
imap_portinteger993IMAP port
imap_usernamestringrequiredIMAP username (or env:VAR_NAME)
imap_passwordstringrequiredIMAP password (or env:VAR_NAME)
imap_use_tlsbooltrueUse direct TLS for IMAP
smtp_hoststringrequiredSMTP host (or env:VAR_NAME)
smtp_portinteger587SMTP port
smtp_usernamestringNoneSMTP username (defaults to IMAP username)
smtp_passwordstringNoneSMTP password (defaults to IMAP password)
smtp_use_starttlsbooltrueUse STARTTLS for SMTP
from_addressstringNoneSender address (defaults to SMTP username)
from_namestringNoneOptional sender display name
poll_interval_secsinteger30How often to check for new email
foldersstring[]["INBOX"]IMAP folders to poll
allowed_sendersstring[][]Optional allowlist (empty = all)
max_body_bytesinteger262144Max inbound body bytes
max_attachment_bytesinteger10485760Max attachment bytes

[messaging.webhook]

KeyTypeDefaultDescription
enabledboolfalseEnable webhook receiver
portinteger18789HTTP listen port
bindstring127.0.0.1Bind address

[[bindings]]

Routes platform conversations to agents. Checked in order; first match wins. Unmatched messages go to the default agent.

KeyTypeDefaultDescription
agent_idstringrequiredWhich agent handles matched messages
channelstringrequiredPlatform name (discord, slack, telegram, twitch, email, webhook)
adapterstringNoneOptional named adapter selector (e.g. ops => discord:ops)
guild_idstringNoneDiscord guild filter
chat_idstringNoneTelegram chat filter
channel_idsstring[][]Discord channel ID filter (includes threads in those channels)