Configuration reference
All AI Workforce OS configuration is read from environment variables. This page lists the variables of the backend (Go), the agent runtime (Python) and the frontend (Next.js), with the default value set in code. It is for anyone installing or operating their own instance.
On this page
- How configuration is loaded
- Server and database
- Authentication, CORS and proxy
- Budget, cost and execution limits
- Rewards and coins
- Runtime: model providers and keys
- Connections, sandbox, storage and push
- Rate limits
- Frontend: build arguments
- Email verification and trial accounts
How configuration is loaded#
The backend reads the environment at startup and validates the combination before it opens its port: if something is inconsistent, the process exits with an error that names the variable. The runtime reads its variables when it uses them. The frontend reads nothing at runtime: its values are fixed at build time.
- Booleans:
true,false,1,0and the other values accepted bystrconv.ParseBool. An invalid value aborts startup. - Integers and durations: a missing, non-numeric or non-positive value is replaced by the default. Durations use Go syntax (
30s,5m,1h). - USD amounts:
0is valid and turns the corresponding cap off; a negative or non-numeric value keeps the default. The exception isBUDGET_USD, which needs a value above zero. - Lists: comma-separated values; spaces are ignored.
Note
In the tables, Conditional means the variable is only required in combination with another one (for example JWT_SECRET with AUTH_ENABLED=true). No means there is a default or the feature stays off.
Server and database#
| Variable | Default | Required | Meaning |
|---|---|---|---|
PORT | 8080 | No | Backend HTTP port. |
DATABASE_URL | empty | Conditional | Postgres URL. Without it the backend keeps everything in memory (demos only). Required with AUTH_ENABLED=true. |
MIGRATE_DATABASE_URL | empty | No | Owner (DDL) connection used only to apply migrations. Empty: DATABASE_URL is used. |
DB_APP_ROLE | app_user | No | Non-owner role without BYPASSRLS that every connection switches to (SET ROLE); the basis of RLS isolation. Empty disables the switch. |
REDIS_URL | empty | No | Redis URL, with the password inside the URL. Empty: events, locks and rate limits live in process memory. |
RUNTIME_URL | http://localhost:8000 | No | Internal URL of the agent runtime. |
RUNTIME_TOKEN | empty | No | Shared secret between backend and runtime. Recommended in production: the backend logs a warning when it is empty. |
READ_HEADER_TIMEOUT | 10s | No | Maximum time to read the headers of a request. |
READ_TIMEOUT | 30s | No | Maximum time to read a whole request. |
WRITE_TIMEOUT | 60s | No | Maximum time per REST request. Not applied to the WebSocket. |
IDLE_TIMEOUT | 120s | No | Idle time of keep-alive connections. |
MAX_BODY_BYTES | 1048576 | No | Maximum size of a request body (1 MiB). |
APP_ENV | empty | No | With the value production, the backend warns when the vault KEK comes from the environment. |
TRACE_RETENTION_DAYS | 30 | No | Days the agents' reasoning traces are kept; they are purged hourly. |
Tip
Set RUNTIME_TOKEN to the same value in the backend and the runtime. Once it is set, the runtime requires Authorization: Bearer <token> on every route except /healthz.
Authentication, CORS and proxy#
With AUTH_ENABLED=false the backend runs in demo mode: one fixed organization and no sign-in. With AUTH_ENABLED=true it enables accounts, JWT, RBAC and per-organization isolation, and the org_id comes from the token.
With authentication on, startup fails if JWT_SECRET is shorter than 32 bytes, if DATABASE_URL is missing or if ALLOWED_ORIGINS contains *.
| Variable | Default | Required | Meaning |
|---|---|---|---|
AUTH_ENABLED | false | No | Enables accounts, JWT, RBAC and multi-tenancy. The production compose file sets it to true. |
JWT_SECRET | empty | Conditional | Access token signing key; at least 32 bytes; required with AUTH_ENABLED=true. Rotating it invalidates every session. |
ALLOWED_ORIGINS | empty | Conditional | Origins allowed for CORS and WebSocket, comma-separated and without a trailing slash. Empty without authentication means *; empty with authentication, same origin only. |
AUTH_COOKIE_SECURE | true | No | Secure attribute of the refresh token cookie. Only turned off for local plain-HTTP development. |
AUTH_COOKIE_NAME | wfai_refresh | No | Name of the refresh token cookie. Change it when several installations share a host. |
TRUSTED_PROXIES | empty | No | CIDRs or IPs of the proxies whose X-Forwarded-For is honored (used by rate limits). An invalid value aborts startup. |
ENABLE_DEMO_RESET | false | No | Registers POST /api/v1/demo/reset, which wipes execution data. Keep it false in production. |
KILL_SWITCH | empty | No | freeze or lockdown: a process-wide override no API call can lift. Re-read on SIGHUP. |
UI_BASE_URL | empty | No | Public frontend URL: where the browser lands after the OAuth callback, and the base of email verification links. |
Warning
With AUTH_ENABLED=false there is no sign-in: anyone who can reach the URL controls the demo organization. Do not expose that mode to the internet with real data or keys.
Budget, cost and execution limits#
These variables control spending and orchestrator concurrency. Context windows and quality checks have more internal settings with sensible defaults; this table covers the ones worth knowing.
| Variable | Default | Required | Meaning |
|---|---|---|---|
BUDGET_USD | 50 | No | Spending cap in USD. The production compose file uses 25 when you do not set it. |
REQUEST_BUDGET_CAP_USD | 0 | No | Hard spending cap per request. 0 turns it off. |
AGENT_BUDGET_USD | 0 | No | Monthly spending cap per agent. 0 turns it off. |
COST_CONFIRM_THRESHOLD_USD | 1.0 | No | Estimated cost above which confirmation is requested before running. |
BUDGET_PAUSE_TIMEOUT | 30m | No | How long a request paused by a cap waits for someone to decide. |
PRICE_IN_PER_M / PRICE_OUT_PER_M | internal price table | No | Force the USD price per million input and output tokens. Set both; the backend and the runtime read them. |
APPROVAL_ACTIONS | send_proposal,send_contract | No | Actions that always require human approval. |
APPROVAL_TIMEOUT | 30m | No | How long an approval inside a request waits. |
STANDING_APPROVAL_DAYS | 30 | No | Lifetime of a standing per-project permission (at most 90). It also ends with the project. |
MAX_PARALLEL / MAX_PARALLEL_PER_ORG | 4 / 8 | No | Concurrent tasks overall and runtime calls in flight per organization; the rest wait in a queue. |
MAX_PLAN_DEPTH | 30 | No | Maximum depth of a plan. |
TASK_TIMEOUT | 120s | No | Maximum time of a task. |
TASK_RETRIES / TASK_MAX_ATTEMPTS | 3 / 2 | No | Retries and maximum attempts per task. |
TASK_RETRY_BACKOFF | 2s | No | Wait between task retries. |
PROJECT_MAX_NODES / PROJECT_MAX_CHILDREN_PER_GROUP / PROJECT_MAX_ACTIVE | 20000 / 200 / 20 | No | Maximum project size and number of active projects per organization. |
PLANNER_MAX_PHASES / PLANNER_MAX_TASKS_PER_PHASE / PLANNER_CONCURRENCY | 12 / 40 / 3 | No | Bounds of the project planner. |
DEP_CONTEXT_TOKEN_BUDGET | 8000 | No | Context tokens a task receives from the tasks it depends on. |
EMAIL_HOLD_SECONDS | 60 | No | Window to cancel an email before it leaves. Minimum 60 and maximum 300: out-of-range values are clamped. |
Rewards and coins#
The coin system rewards completed work. REWARDS_RULES takes a JSON object that is applied on top of the default rules, so you only include the fields you want to change.
If the JSON has an invalid value, the backend logs it and uses the full default rules: it never applies a half-changed economy.
{
"enabled": true,
"base": { "S": 5, "M": 10, "L": 20, "XL": 40 },
"default_complexity": "M",
"first_try_bonus_pct": 50,
"rework_pct": 50,
"project_base": 20,
"project_per_task": 2,
"project_max": 100,
"min_project_tasks": 2
}
| Variable | Default | Required | Meaning |
|---|---|---|---|
REWARDS_ENABLED | true | No | false, 0 or off turn coin earning off entirely. |
REWARDS_RULES | empty | No | JSON with rules: enabled, base (S, M, L, XL), default_complexity, first_try_bonus_pct, rework_pct, project_base, project_per_task, project_max, min_project_tasks. |
REWARDS_WELCOME_COINS | 1000 | No | Welcome coins each organization receives once, the first time it opens the shop or the office. 0 turns it off. |
Runtime: model providers and keys#
The runtime supports five providers: deepseek, anthropic, openai, kimi and custom (an OpenAI-compatible endpoint such as vLLM or Ollama). A provider counts as configured when its key (and, for custom, its base URL) is in the environment. With no provider configured, or with SIMULATION=true, the runtime uses the simulated engine: scripted answers, no keys and no cost.
On a transient error (timeout, rate limit, 5xx) it retries on the same provider and then moves to the next allowed one. ALLOWED_PROVIDERS is the operator ceiling: no data reaches an excluded provider, not even as a fallback. Each organization can narrow it further in its settings, never widen it, and can store its own provider key, which wins over the environment's.
| Variable | Default | Required | Meaning |
|---|---|---|---|
SIMULATION | false | No | true forces the simulated engine even when keys are present. |
DEEPSEEK_API_KEY / DEEPSEEK_MODEL | empty / deepseek/deepseek-chat | Conditional | DeepSeek key and model; DeepSeek is the platform default provider. |
ANTHROPIC_API_KEY / ANTHROPIC_MODEL | empty / anthropic/claude-sonnet-5-5 | Conditional | Anthropic key and model. |
OPENAI_API_KEY / OPENAI_MODEL / OPENAI_BASE_URL | empty / openai/gpt-4.1 / empty | Conditional | OpenAI key, model and optional base URL. |
KIMI_API_KEY / KIMI_MODEL / KIMI_BASE_URL | empty / openai/kimi-k2-0905-preview / https://api.moonshot.ai/v1 | Conditional | Kimi (Moonshot) key, model and base URL. |
CUSTOM_LLM_BASE_URL | empty | Conditional | Base URL of an OpenAI-compatible endpoint. Enables the custom provider. |
CUSTOM_LLM_API_KEY / CUSTOM_LLM_MODEL | empty | No | Optional key (many local servers need none) and model name of the custom endpoint. |
CUSTOM_PRICE_IN_PER_M / CUSTOM_PRICE_OUT_PER_M | 3 / 15 | No | USD per million tokens of the custom endpoint. May be 0 for a local model. |
MODEL | empty | No | Legacy variable: provider/model applies to that provider; an unprefixed name applies to the primary. |
MODEL_PROVIDER_ORDER | deepseek,anthropic,openai,kimi,custom | No | Global fallback order. MODEL_PROVIDER_ORDER_<ROLE> changes it for an agent role or a call type (for example MODEL_PROVIDER_ORDER_PLAN). |
ALLOWED_PROVIDERS | empty | No | Operator ceiling: only these providers may receive data. Empty means all configured. Set it the same in the backend and the runtime. |
PROVIDER_RETRIES | 1 | No | Retries on the same provider before moving to the next. |
PROVIDER_RETRY_BACKOFF_S | 1.0 | No | Wait between retries, in seconds. |
PROVIDER_TIMEOUT_S | 120 | No | Timeout per provider call, in seconds. |
SYNTH_PROMPT_TOKEN_BUDGET | 24000 | No | Token budget of the synthesis prompt in the runtime. |
Warning
Provider keys are secrets that cost money when they leak. Pass them through the service environment, never in an image or the repository.
Connections, sandbox, storage and push#
Without CONNECTIONS_KEK no live credential can be stored: the vault fails closed and only simulated connections work. The KEK is base64 of 32 random bytes; keep it apart from the database, because without it the encrypted secrets cannot be recovered.
The code sandbox is optional and needs the sandbox-runner service, which is given access to the Docker socket. Read the security model page first.
openssl rand -base64 32
| Variable | Default | Required | Meaning |
|---|---|---|---|
CONNECTIONS_KEK | empty | No | Key-encryption key of the vault (base64 of 32 bytes). Needed by live connections and by each organization's own model keys. |
CONNECTIONS_KEK_PREVIOUS | empty | No | Previous KEK, only while a rotation is in progress. |
CUSTOM_API_ALLOW_PRIVATE | false | No | When true, generic API sources (custom_api) may reach private addresses, such as an ERP on your LAN. Only for self-hosted installs on a trusted network; it never changes the GET-only rule. |
GOOGLE_OAUTH_CLIENT_ID / GOOGLE_OAUTH_CLIENT_SECRET | empty | No | Your own Google OAuth app for connections. |
LINKEDIN_OAUTH_CLIENT_ID / LINKEDIN_OAUTH_CLIENT_SECRET | empty | No | Your own LinkedIn OAuth app. |
OAUTH_REDIRECT_URL | http://localhost:8080/api/v1/connections/oauth/callback | No | Public URL of the OAuth callback; must match the one registered with the provider. |
GITHUB_API_URL | empty (api.github.com) | No | GitHub API base, for example for GitHub Enterprise. |
SANDBOX_URL / SANDBOX_TOKEN | empty | No | Address and token of the sandbox-runner. Without both, agents cannot run code. |
SANDBOX_IMAGE / SANDBOX_RUNTIME / SANDBOX_SECCOMP_PROFILE | empty | No | Read by the sandbox-runner: execution image, container runtime (for example gVisor's runsc) and seccomp profile. |
S3_ENDPOINT / S3_BUCKET / S3_REGION / S3_PREFIX / S3_USE_PATH_STYLE | empty (S3_USE_PATH_STYLE: true) | No | S3-compatible storage for Drive files. Without endpoint and bucket, Postgres is used, or memory when there is no database. |
S3_ACCESS_KEY / S3_SECRET_KEY | empty | No | S3 credentials. |
DRIVE_MAX_FILE_MB / DRIVE_QUOTA_MB | 25 / 5120 | No | Maximum size per file and quota per organization, in MB. |
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT | empty | No | All three together enable Web Push notifications; without them they stay off. |
PUSH_ALLOWED_HOSTS | empty | No | Push service hosts added to the default allow-list. |
Rate limits#
Rate limits are built into the backend and have no tuning variables: for example, 10 sign-in attempts per 15 minutes and 120 commands per minute. The authentication limits are stored in Redis when REDIS_URL is set, and in process memory otherwise.
What you do configure is TRUSTED_PROXIES. Only the X-Forwarded-For header from those proxies is used to identify the client. Behind Caddy, list the proxy network: without it, every client shares the proxy's IP and the same counter.
Frontend: build arguments#
The frontend is compiled with standalone output. NEXT_PUBLIC_* variables are baked in at build time as Dockerfile arguments (ARG); changing them means rebuilding the image.
Leaving them empty gives the recommended single-domain behavior: the API at the relative path /api/v1 and a WebSocket URL derived from window.location.
docker build \
--build-arg NEXT_PUBLIC_API_URL=https://app.example.com/api/v1 \
--build-arg NEXT_PUBLIC_WS_URL=wss://app.example.com/ws \
-t aiworkforce-frontend ./frontend
| Build argument | Default | Required | Meaning |
|---|---|---|---|
NEXT_PUBLIC_API_URL | empty | No | Absolute API base URL, for example https://app.example.com/api/v1. Empty: relative path. |
NEXT_PUBLIC_WS_URL | empty | No | Absolute WebSocket URL, for example wss://app.example.com/ws. Empty: derived from window.location. |
NEXT_PUBLIC_MOCK | empty | No | With the value true, the frontend runs on mock data, without a backend. |
Email verification and trial accounts#
Two optional sign-in features, both requiring AUTH_ENABLED=true. Email verification needs a working SMTP server: with EMAIL_VERIFICATION=true and no valid SMTP the backend refuses to start, so a half-configured server cannot lock anyone out.
Trial accounts let people use the app without signing up; each one gets its own organization and stops working on its own.
| Variable | Default | Meaning |
|---|---|---|
EMAIL_VERIFICATION | false | New accounts must open a one-time link sent to their address before they can sign in. Needs AUTH_ENABLED, UI_BASE_URL and SMTP_* (or EMAIL_DEV_LOG). |
EMAIL_DEV_LOG | false | Development only: verification links are written to the log instead of being sent. |
SMTP_HOST | empty | SMTP server. Required with EMAIL_VERIFICATION unless EMAIL_DEV_LOG is on. |
SMTP_PORT | 465 | 465 uses implicit TLS; 587 uses STARTTLS. |
SMTP_TLS | tls on port 465; starttls otherwise | tls, starttls or none (none only for a local relay). |
SMTP_USER / SMTP_PASS | empty | SMTP server credentials; set both or neither. |
SMTP_FROM | empty | Sender, for example AI Workforce OS <no-reply@example.com>. Required for SMTP verification. |
TRIAL_ENABLED | false | Turns on POST /api/v1/auth/trial: throwaway accounts with their own organization. |
TRIAL_TTL_HOURS | 24 | Hours a trial account works. |
TRIAL_PER_IP_PER_DAY | 3 | Trial accounts one IP address may create per day. |
TRIAL_GLOBAL_PER_DAY | 300 | Trial accounts created per day across all addresses. |
TRIAL_EMAIL_DOMAIN | prueba.invalid | Domain of the generated trial addresses (no mailbox behind it). |