File path
<FLOWLY_HOME>/config.jsonFLOWLY_HOME defaults to ~/.flowly. The file is stored with owner-only (0600) permissions because it holds API keys.
Sibling files in the same directory:
config.json.bak— a self-heal backup, seeded from the last good config.config.json.broken-<unix-ts>— a corrupted config moved aside during recovery.
Profiles
A profile is an isolated FLOWLY_HOME with its own config, sessions, workspace, and credentials. The active profile is resolved in this order:
-p/--profile <name>CLI flagFLOWLY_PROFILEenvironment variable~/.flowly/active_profilesticky pointer file"default"
The default profile is ~/.flowly. A named profile coder lives at ~/.flowly/profiles/coder. Profile names must match ^[a-z0-9][a-z0-9_-]{0,63}$; the names flowly, default, test, tmp, root, and sudo are reserved. See Environment variables.
Self-healing loader
The loader is resilient by design:
- It parses
config.json. If valid, it's used (and a.bakis seeded if missing). - If parsing or validation fails, it tries
config.json.bak. If that's good, the broken file is renamedconfig.json.broken-<ts>and the backup is restored. - If both fail, Flowly falls back to in-code defaults so the agent still boots.
config.json and config.json.bak fail to load, secrets in the broken file are lost from the running process (but the broken file is preserved on disk).
unknown or manually-added keys are preserved. A None value in the model never overwrites an existing non-None value on disk — to truly clear a value, write "" or delete the key. Unknown top-level keys are ignored (extra = "ignore"), not errors. A handful of behaviors are overridable with the documented FLOWLY_* environment variables; note that a generic nested override like FLOWLY_GATEWAY__PORT only takes effect on the fallback path (when config.json is missing or unparseable) — when a valid config.json exists, it is not consulted for those.
Top-level keys
| Key | Purpose |
|---|---|
agents | Agent defaults, compaction, heartbeat, memory search, multi-agent |
channels | Messaging channels (Telegram, Discord, Slack, …) |
providers | LLM providers and API keys |
gateway | Gateway host / port |
tools | Built-in tool toggles and limits |
integrations | Third-party integrations (Trello, voice, X, …) |
audit | Local audit-log retention |
plugins | Enable / disable plugins |
mcpServers | MCP server definitions |
backgroundMode | bool (default false) |
agents
agents.defaults — applied to the main agent:
| Key | Default |
|---|---|
workspace | "~/.flowly/workspace" |
cwd | "" (empty → workspace; overridden by FLOWLY_CWD) |
model | "anthropic/claude-haiku-4.5" |
maxTokens | 8192 |
temperature | 0.7 |
actionTemperature | 0.1 |
actionToolRetries | 2 |
maxToolIterations | 100 |
softWarnAtIteration | 30 |
contextMessages | 100 |
persona | "default" |
saveTrajectories | false |
memoryNudgeInterval | 10 |
skillNudgeInterval | 15 |
agents.defaults.compaction — see Sessions:
| Key | Default |
|---|---|
mode | "safeguard" ("default" | "safeguard") |
reserveTokensFloor | 20000 |
maxHistoryShare | 0.5 |
contextWindow | 128000 |
memoryFlush.enabled | true |
memoryFlush.softThresholdTokens | 4000 |
agents.defaults.goals — see Standing goals:
| Key | Default |
|---|---|
enabled | true |
maxTurns | 20 (turn budget before a goal pauses for review) |
judgeProvider | "" (empty inherits the conversation's provider) |
judgeModel | "" (empty inherits the conversation's model) |
judgeTimeoutSeconds | 30 |
judgeMaxTokens | 4096 |
gateTimeoutSeconds | 300 |
gateMaxRetries | 3 |
agents.defaults.heartbeat:
| Key | Default |
|---|---|
enabled | true |
everyMinutes | 30 |
activeHours | null (or { start: "09:00", end: "23:00", timezone: "" }) |
deliver | "none" ("none" | "message_tool") |
agents.defaults.memorySearch:
| Key | Default |
|---|---|
enabled | true |
provider | "auto" ("auto" | "openai" | "gemini" | "none") |
model | "" |
apiKey | "" |
apiBase | "" |
chunkTokens | 400 |
overlapTokens | 80 |
maxResults | 6 |
minScore | 0.35 |
vectorWeight | 0.7 |
textWeight | 0.3 |
agents.defaults.memoryDreaming — cross-session "dreaming" + autonomous consolidation (see Memory):
| Key | Default |
|---|---|
enabled | true (the whole governance/dreaming layer) |
commitMode | "selective" ("selective" | "manual" | "aggressive") |
idleMinutes | 30 (run after this much agent inactivity; background heartbeats don't count) |
dailyEnabled | true |
dailyTime | "03:30" (HH:MM local) |
turnInterval | 10 (also run every N user turns; 0 disables the coarse pass) |
autoFloor | 0.80 (≥ → auto-active when unconflicted and not sensitive) |
reviewFloor | 0.55 (< → dropped instead of queued) |
maxMessagesPerRun | 500 (bound per pass so a backlog can't blow up one run) |
autoConsolidate | true (background cleanup: merge duplicates, retire stale) |
consolidateTurnInterval | 50 (consolidate every N user turns; 0 off) |
consolidateEveryMinutes | 30 (background consolidation timer; 0 off) |
freezeInjectedMemory | false (advanced: freeze the injected memory block per session for prefix-cache stability) |
agents.agents (per-agent map) — name, provider ("anthropic" default | "openai" | "flowly"), model="", workingDirectory="", persona="".
agents.teams (per-team map) — name, agents=[], leaderAgent="".
channels
Channels are off by default. See Channels overview.
| Channel | Key defaults |
|---|---|
whatsapp | enabled=false, bridgeUrl="ws://localhost:3001", allowFrom=[] |
telegram | enabled=false, token="", allowFrom=[], dmPolicy="pairing" ("open"|"pairing"|"allowlist") |
discord | enabled=false, token="", allowFrom=[], gatewayUrl="wss://gateway.discord.gg/?v=10&encoding=json", intents=37377 |
slack | enabled=false, mode="socket", botToken="", appToken="", groupPolicy="mention", groupAllowFrom=[], dm.enabled=true, dm.policy="open", dm.allowFrom=[] |
web | enabled=false, relayUrl="", serverId="", authToken="", jwtSecret="" |
email | enabled=false, pollIntervalSeconds=30, allowFrom=[] |
teams | enabled=false, webhookUrl="", defaultChatLabel="", allowFrom=[] |
providers
| Key | Default / notes |
|---|---|
active | "" — explicit default provider slug; "" falls back to the API-key cascade |
flowly | enabled=true, apiBase="https://useflowlyapp.com/api/v1" (Flowly Cloud; uses account token when signed in) |
xaiOAuth | enabled=true, clientId="", apiBase="https://api.x.ai/v1" (tokens stored in the OS keychain, not config) |
openaiCodex | ChatGPT subscription (Codex OAuth) — tokens in the OS keychain, not config |
zaiCoding | Z.AI GLM Coding Plan — tokens in the OS keychain, not config |
BYOK provider slots — each with apiKey="", apiBase=null, fallbackKeys=[]: anthropic, openai, openrouter, zhipu, sakana, vllm, gemini, groq, xai.
When active="", the cascade picks the first usable provider in priority order:
openrouter → anthropic → openai → openai_codex → zai_coding → xai
→ xai_oauth → gemini → groq → zhipu → sakana → vllmSetting a provider explicitly (/provider, flowly setup, or active in this file) skips the cascade entirely — worth doing if more than one credential is present, so the choice is yours rather than the order's. See Providers and models.
openai_codex
falls back to the Codex CLI's ~/.codex/auth.json, and zai_coding to
OpenCode's auth.json. That's why an existing subscription can work with
no setup — and why flowly setup asks before using one instead of
adopting it silently.
gateway
| Key | Default |
|---|---|
host | "127.0.0.1" |
port | 18790 (1–65535) |
token | "" — static gateway administration token; the CLI ensures one on a non-loopback bind |
The dedicated POST /api/mcp/manage route always requires a valid, non-empty
gateway token, including through an SSH loopback forward. An empty token may
retain legacy local behavior on other routes; it never unlocks MCP management.
The token is not an external-agent MCP access key. SSH passwords and host-key
verification are client responsibilities, not gateway or mcpServers settings.
See Remote MCP setup and the
management API.
tools
See Sandbox and approvals for execution policy.
| Tool | Key defaults |
|---|---|
web.search | Brave defaults (apiKey="", maxResults=5, proxyUrl="") + backend selectors (backend/searchBackend/extractBackend) + per-backend sub-sections (ddgs, searxng, tavily, exa, firecrawl, parallel). See Web & research. |
exec | enabled=true, timeoutSeconds=300, maxOutputChars=200000, approvalTimeoutSeconds=120, cronMode="deny" ("deny"|"approve") |
artifact | enabled=true, maxContentLength=500000 |
browserTab | enabled=false |
computer | enabled=false, actionDelayMs=100, failsafe=true |
codexSession | enabled=false, codexBin="codex", codexHome="", cwd="", turnTimeoutS=600, postToolQuietTimeoutS=90, approvalPolicy="on-request", sandbox="workspace-write", exposeFlowlyTools=true |
not in config.json — only enabled and the runtime knobs above are. Approval policy lives in the approvals store at ~/.flowly/credentials/exec-approvals.json. See Sandbox and approvals.
integrations
| Integration | Key defaults |
|---|---|
trello | apiKey="", token="" |
voice | enabled=false, bridgeUrl="http://localhost:8765", plus Twilio / STT / TTS credentials (sttProvider="groq", ttsProvider="elevenlabs", ttsVoice="21m00Tcm4TlvDq8ikWAM", language="en-US") |
x | bearerToken, apiKey, apiSecret, accessToken, accessTokenSecret (all "") |
googleWorkspace | enabled=false, email="" |
linear | apiKey="" |
homeAssistant | url="", token="" (tools register only when both are set) |
audit
| Key | Default |
|---|---|
enabled | true |
retentionDays | 90 (-1 disables the age cap) |
maxSizeMb | 100 (0 disables the size cap) |
Audit records are written as daily JSONL files under <FLOWLY_HOME>/audit/. These keys control retention only.
plugins
| Key | Default |
|---|---|
enabled | [] |
disabled | [] |
Bundled plugins load by default unless listed in disabled. User plugins under $FLOWLY_HOME/plugins/<name>/ load only if listed in enabled. disabled overrides enabled.
mcpServers
A map of server name → server config. Each server uses either local stdio (command, args, env) or HTTP/SSE (url, headers). Common defaults include enabled=true, transport="auto", protocol="auto", timeout=120, and connectTimeout=60. HTTP connections can use native OAuth with auth="oauth"; TLS, mTLS, scope, trust, elicitation, bounded sampling, diagnostics, pagination, binary-content, concurrency, and supervised lifecycle controls are available per server.
Tool permissions use tools.mode: all, selected, none, or legacy. selected with an empty include list means no tools. legacy preserves older hand-written behavior, where a non-empty include list wins, otherwise exclude applies, otherwise all tools load. Resource and prompt utilities are separately controlled by tools.resources and tools.prompts.
In all and selected modes, exclude still removes matching tool names. all includes future discoveries; use selected to pin exact tool names. App confirmation of none disables the connection and clears resource/prompt access. Configuration publication and live application are separate steps: an apply failure can leave the new settings saved, so inspect the reported runtime state and retry.
App-managed setup writes an internal oauthCredentialId only after OAuth, connection testing, discovery, and explicit permission confirmation succeed. Do not copy that identifier between server entries. Server names and env/headers keys are preserved verbatim by the loader. See the complete MCP configuration reference for every key, default, bound, and lifecycle behavior.
Example
A minimal config.json after setting an Anthropic key and a Telegram bot:
{
"providers": {
"active": "anthropic",
"anthropic": { "apiKey": "sk-ant-..." }
},
"agents": {
"defaults": {
"model": "claude-sonnet-4-5",
"persona": "default"
}
},
"channels": {
"telegram": { "enabled": true, "token": "123:ABC", "dmPolicy": "pairing" }
},
"gateway": { "host": "127.0.0.1", "port": 18790 }
}