Skip to main content

Configuration reference

Every setting below is an environment variable. The backend reads them from backend/.env (backend/.env.example is the annotated template); the two frontend variables are read from the frontend's own process environment. The model and API-key settings have a page of their own under LLM configuration.

Server settings​

VariableDefaultWhat it does
HOST0.0.0.0Address the backend binds to
PORT8000Port the backend binds to
RELOADfalseuvicorn autoreload for local development. Only read when the backend is started as python -m backend.main; the Docker image's own start command is unaffected either way
TIMEZONEUTCIANA name of the deployment's local time zone. It decides where a calendar day starts for the operations metrics — the "today" counts and the daily buckets of the lead-time trend — and is the zone the workflow agent reads a task's time requirement in when the requirement names no time zone. An unrecognized name falls back to UTC rather than failing startup, so a typo skews local times instead of stopping the app

Frontend settings​

VariableDefaultWhat it does
BACKEND_BASE_URLhttp://localhost:8000Where the frontend's server-side proxy forwards /api/*. Read once at process start. In the Docker Compose stack this is the backend's address on the internal network, not a public one
FRONTEND_PORT3000Docker Compose only: the host port the frontend is published on. The container keeps listening on 3000 internally, and the backend's CORS_ORIGINS follows this automatically

Application database​

VariableDefaultWhat it does
DB_URLsqlite:///a2flow.dbDatabase URL. SQLite (relative to the working directory) and PostgreSQL are supported

The async driver suffix (sqlite+aiosqlite / postgresql+asyncpg) is added automatically, so the plain scheme is enough:

DB_URL=postgresql://user:password@localhost:5432/a2flow

Schema changes are tracked as versioned Alembic migrations and applied automatically on startup, so redeploying the app is what brings the schema up to date. Running more than one backend replica requires PostgreSQL — see Horizontal scaling. What each table holds is in Database.

Seeded users​

On startup the backend seeds a hidden system user, plus two real accounts, each created only on the very first startup that finds its target record missing:

  • An initial root user holding the super_admin role (see Authorization), platform-scoped (tenantId: null). Skipped once any real (non-system) user already exists, so it runs only on the very first startup.
  • A Default tenant (slug: default) and, inside it, an initial admin user holding the admin role. The tenant (by slug) and the user (by username scoped to that tenant) are checked independently, so either can be recreated without duplicating the other.

The hidden system user owns the bootstrap records (it cannot log in and is excluded from the user list).

VariableDefaultWhat it does
ROOT_PASSWORDgeneratedPassword for the seeded root user
ADMIN_PASSWORDgeneratedPassword for the seeded admin user in the Default tenant

If either is unset (or empty), a random password is generated instead and logged once, at WARNING level, when that user is created — it cannot be recovered once the log line has scrolled past. Set both explicitly before the first run for anything beyond local experimentation, or capture the generated passwords from the startup logs immediately and change them afterwards. The usernames are fixed to root and admin.

Session lifetime​

VariableDefaultWhat it does
SESSION_IDLE_TIMEOUT_SECONDS28800 (8 hours)Sliding idle timeout. Each authenticated request refreshes the session's last-active time; a session left idle longer than this is rejected and deleted
SESSION_COOKIE_SECUREfalseMarks the session and CSRF cookies Secure (HTTPS only). Set it to true for any deployment behind HTTPS

The cookies themselves are session cookies (no Max-Age/Expires), so they are also cleared when the browser closes. The frontend reaches the backend through a same-origin rewrite (/api/*), so the cookies are first-party and SameSite=Lax applies cleanly.

CORS​

VariableDefaultWhat it does
CORS_ORIGINShttp://localhost:3000Comma-separated list of origins allowed to call the backend API

Add each origin the frontend is served from:

CORS_ORIGINS=https://app.example.com,http://localhost:3000

* is rejected at startup — allow_credentials=True is always enabled, and pairing it with a wildcard origin is invalid per the CORS spec.

Agent skill store​

VariableDefaultWhat it does
SKILLS_DIRbackend/.skillsRoot of the store Agent Skill repositories are shallow-cloned into. Under docker compose it is /var/lib/a2flow/skills, backed by the skills named volume
SKILLS_PRUNE_GRACE_SECONDS3600How long a revision directory survives regardless of whether anything references it. A pull prunes revisions no execution is pinned to, and the grace window covers the gap between a run reading the current revision and inserting the row that names it
SKILLS_CLONE_TIMEOUT_SECONDS120Bounds how long a clone's individual HTTP requests may take. Without it a hanging remote stalls the clone indefinitely, leaving the skill pending

The store holds one immutable directory per revision, and a revision is never modified once published, so a pull never disturbs an agent loading an existing one:

$SKILLS_DIR/<agent_skill_id>/<commit_sha>/

This is durable state, not a cache: a workflow execution pins the revision it started with, so wiping the directory leaves existing executions unable to load their skill (HTTP 409 SKILL_NOT_READY) until an admin pulls the skill again — and a pull fetches the repository's current head, not the pinned revision. Back it up (Backup and restore), and give every backend replica the same directory (Horizontal scaling).

Knowledge bases​

VariableDefaultWhat it does
KNOWLEDGE_SOURCE_MAX_BYTES20971520 (20 MiB)Largest document that may be uploaded to a knowledge base, and largest web page fetched for one. A larger upload is refused as soon as it passes this
KNOWLEDGE_INGEST_CHUNK_CHARS40000Characters of a source the LLM reads at a time. A longer source is read in parts, one after another. Lower it for a model with a small context window
KNOWLEDGE_FETCH_TIMEOUT_SECONDS30How long fetching a web page source may take per request

Sources and pages are stored in the database, like session files, so they need no volume of their own. Reading a source calls the LLM configured for the agents (LLM configuration), several times for a large document.

Session files​

VariableDefaultWhat it does
SESSION_FILE_MAX_BYTES20971520 (20 MiB)Largest single file that may be attached to a workflow session. An upload is refused as soon as it passes this, so a larger body is never held whole
SESSION_FILES_MAX_TOTAL_BYTES209715200 (200 MiB)Combined size of everything one session holds. Files are removed only when their run is deleted, so this is what bounds a long-running session

Session files are stored in the database rather than on disk, so they need no directory of their own, no shared volume across replicas, and no backup step beyond the database's own (Backup and restore). Raising these limits raises how much of the database one run can occupy, and how much memory a single upload or download takes — the bytes are read whole.

Secret management​

VariableDefaultWhat it does
SECRET_ENCRYPTION_KEY—Fernet key used to encrypt local secrets before storage
SECRET_KEY_FILE.secret_keyPath to the on-disk key file, next to the SQLite database file. Under docker compose it is /var/lib/a2flow/secret-key/.secret_key, backed by the secret-key named volume that the backend and the email worker share

The encryption key is resolved at first use, in order:

  1. SECRET_ENCRYPTION_KEY, which must be a valid Fernet key.
  2. The key file at SECRET_KEY_FILE.
  3. Failing both, a key is generated, saved to that file, and a WARNING is logged.

Back the key up. Losing it makes every stored local secret undecryptable, and the same key protects the approval CA's signing key.

There is no environment variable for HashiCorp Vault: a tenant's own Vault is set by its admin under Tenant Settings, and the global Vault's address by a Super Admin under System Settings, with each tenant's credential for it on the tenant's page. A tenant's own Vault address goes through the same SSRF checks as other user-supplied URLs, so it must be public. The global Vault address is exempt, since only a Super Admin can set it and a Vault typically lives on a private address.

MCP tools and approvals​

Every MCP tool call must present a short-lived X.509 certificate issued for the task making it — granted when the task's approval is, or when a task nobody was asked to approve starts. The signing root is generated on first use and stored encrypted with the same key as local secrets, so nothing here needs configuring for the feature to work.

VariableDefaultWhat it bounds
MCP_TOOL_CERT_TTL_SECONDS3600How long a task's certificate stays valid — the window in which it may call its tools
MCP_TOOL_CERT_SIGNATURE_WINDOW_SECONDS60Clock-skew tolerance for the proof-of-possession signature accompanying each proxied call
MCP_CA_COMMON_NAME / MCP_CA_VALIDITY_DAYSA2Flow MCP Approval CA / 3650Subject and lifetime of the generated root. Read only when the root is first generated; changing them later has no effect on an existing root
MCP_REGISTRY_URLhttps://registry.modelcontextprotocol.ioBase URL of the registry the Browse registry dialog searches
MCP_ELICITATION_TIMEOUT_SECONDS900How long a tool call waits for the run's initiator to answer an MCP server's confirmation before the question expires. The initiator has just resumed the task, so this bounds only a question left behind
AGENT_TURN_TIMEOUT_SECONDS3600The longest one agent turn of a run may last, waiting included. Keep it well above MCP_ELICITATION_TIMEOUT_SECONDS, since a turn whose tool call is waiting on a person is still running

MCP proxy​

A registered MCP server is not A2Flow's code, so it runs somewhere separate from the agent and its secrets. The Docker Compose deployment sets all of this up; a deployment that assembles the containers itself sets it here.

Left unset — which is what a plain local run does — the agent reaches MCP servers itself, in its own process. That is convenient for development and is not what you want in an environment where anyone can register a server.

VariableDefaultWhat it does
MCP_PROXY_URLunsetWhere the proxy is. Setting it is what moves MCP servers out of the agent's process
MCP_PROXY_SERVER_NAME / MCP_PROXY_PORTmcp-proxy / 8443The name the proxy is reached by, and the port it listens on. The name is part of its certificate, so both ends have to agree
MCP_PROXY_TLS_DIRa directory beside the applicationWhere the agent publishes what the proxy needs to identify itself. The proxy reads the same directory, read-only
MCP_BACKEND_TLS_DIRa directory beside the applicationWhere the agent keeps its own key. Deliberately not the directory above — nothing running in the proxy should be able to read it
MCP_TRANSPORT_CERT_VALIDITY_DAYS365Lifetime of that material. Reissued automatically once fewer than 30 days remain, so it needs no attention
MCP_SANDBOX_DIRunsetWhere each tenant's home directory for sandboxed server launches lives — the packages a tenant's servers download are kept here, one directory per tenant. Set, every locally started server runs in its own sandbox; the proxy refuses to start without it. Unset on a plain local run, where servers start unsandboxed

Notification email​

These are the same values the admin UI edits under System Settings, settable from the environment so a deployment can ship its mail configuration rather than leaving it as a manual step.

VariableDefaultWhat it does
APP_BASE_URL—Base URL at which users reach this deployment in a browser, e.g. https://a2flow.example.com. Used to build the deep links in outgoing notification email; without it, messages go out with no link back
SMTP_ENABLEDfalseMaster switch for email delivery. Must be explicitly true; it is never inferred from the other variables being set
SMTP_HOST / SMTP_PORT— / 587The relay to hand messages to. A bare internal hostname is fine here — this is handed to the SMTP client, not fetched
SMTP_SECURITYstarttlsnone, starttls, or ssl
SMTP_USERNAME / SMTP_PASSWORD—SMTP AUTH credentials. The password is only ever stored as ciphertext, never logged, and never returned by any API response
SMTP_FROM_EMAIL / SMTP_FROM_NAME—Sender address, required once delivery is on, and the optional display name beside it

Unlike everything else on this page, these are not read fresh on each use — they are written onto the single settings record. That gives them their own rules:

  • Re-applied on every startup, not just the first, so rotating a relay password is a redeploy.
  • Only the variables you actually set are written, so leaving one unset preserves whatever is stored — including anything an admin configured by hand in the UI.
  • A malformed value is logged as a warning and skipped entirely. A bad port, an invalid address, or SMTP_ENABLED=true with no host leaves the stored configuration untouched; the app still starts.

Because the enable flag is separate, a relay's host and credentials can be staged in one deploy and delivery switched on in a later one.

Outgoing email queue​

These are the knobs for the queue that drains into the relay — see The delivery queue for how it behaves. Nothing here needs setting for a normal deployment.

VariableDefaultWhat it does
EMAIL_WORKER_IN_PROCESStrueWhether the API process also runs the drain worker, so uvicorn main:app on its own delivers mail. Set it false when running a dedicated worker process — see Process layout. Leaving both on is safe, just pointless: an advisory lock elects exactly one sender across the deployment
EMAIL_SEND_RATE_PER_SECOND5.0Messages per second handed to the relay; the worker pauses 1 / rate before each send. Lower it to sit under a stricter relay limit
EMAIL_QUEUE_BATCH_SIZE / EMAIL_QUEUE_POLL_INTERVAL_SECONDS20 / 5.0How many messages one drain pass claims, and how long the worker sleeps when the queue is empty. The poll interval is the floor on delivery latency for a notification produced while the worker is asleep
EMAIL_MAX_ATTEMPTS9Delivery attempts before a message becomes a dead letter. The backoff runs 15s, 30s, 1m, 2m and so on, capped at an hour, so the default rides out roughly an hour of relay downtime. A failure the relay reports as permanent is written off on the first attempt regardless
EMAIL_SENT_RETENTION_DAYS30How long delivered messages are kept before the worker purges them. They are a record of what went out, not queue state. Dead letters are never purged

Agent​

VariableDefaultWhat it does
EXECUTION_MAX_PARALLEL_SESSIONS4Most sessions one workflow run works in at once — its main session plus the branch sessions forked when its task graph branches. At the limit, a ready task waits for its session instead of forking another. 1 turns forking off. At least 1
ROLE_DESCRIPTIONA generic assistant descriptionRole text the agent's system prompt is built around, alongside the workflow rules and the interface schema
SESSION_RUNNER_CONCURRENCY4How many turns one replica runs at once on a run's own behalf — kickoffs, and resumptions after an approval is decided. Each is a live conversation with the model, so this bounds a replica's model calls for runs nobody is watching. At least 1