Configuration
All configuration is via environment variables. No config files are required. The engine reads .env if present (via dotenv).
Runtime & Logging
| Variable | Default | Description |
|---|---|---|
NODE_ENV |
development |
development, production, or test |
DEBUG |
false |
Enable verbose validation errors. Also switches deferred retry delays from seconds to milliseconds – intended for tests, never for production |
LOG_LEVEL |
info |
error, warn, info, debug. Defaults to debug when DEBUG=true |
LOG_FORMAT |
pretty |
json (production) or pretty (development). Auto-selects json in production |
PORT |
3030 |
HTTP server port |
HTTP
| Variable | Default | Description |
|---|---|---|
| Most routes accept a request body of up to 100 kB. Two routes take bulk payloads from workers | ||
| and are raised to 50 MB: |
POST /blackboard/job/result– job results, which can carry bulk entity updatesPOST /blackboard/job/log– job execution logs
| Variable | Default | Description |
|---|---|---|
MAX_REQUEST_BODY_SIZE |
(unset) | Replaces the 50 MB limit on the two routes above, e.g. 100mb or a plain byte count. Leave unset to keep the built-in limit |
Set this when workers push results larger than 50 MB and get 413 Payload Too Large.
It does not change the 100 kB default
MAX_REQUEST_BODY_SIZE only adjusts the routes that are already raised. Every other route,
including workflow publishing, keeps the 100 kB limit and is unaffected by this variable.
A malformed value aborts startup
The value is checked when the engine starts, and anything that is not a byte size stops the boot with an error. This is deliberate: an unparseable limit would otherwise be read as unlimited, so a typo would silently remove the cap instead of setting it.
License
The license is compiled into the image and cannot be changed at runtime – there is no
environment variable that grants or upgrades one. To see which license is active and how much of
each quota is in use, call GET /license/; see
API Reference.
The build-time variables behind it are documented under Docker Deployment.
Database
The engine requires PostgreSQL 15+ and is tested against 17 (the version docker-compose.yml runs).
| Variable | Default | Description |
|---|---|---|
POSTGRES_HOST |
localhost |
Database hostname |
POSTGRES_PORT |
5434 |
Database port |
POSTGRES_USER |
postgres |
Database user |
POSTGRES_PASSWORD |
unsafe |
Database password (override in production) |
Change the default password
The default password unsafe is for local development only. Always set a strong
password in production deployments.
Database Setup
Run migrations to create the schema:
npx mikro-orm migration:fresh # development (drops and recreates)
npx mikro-orm migration:up # production (applies pending migrations)
CMS Connection
The engine communicates with the neops CMS via GraphQL for entity acquisition and locking.
| Variable | Default | Description |
|---|---|---|
NEOPS_CMS_URL |
http://localhost:8000/graphql |
CMS GraphQL endpoint |
NEOPS_CMS_TOKEN |
unsafe |
Authentication token for CMS API (override in production) |
CMS dependency
The CMS is required for entity acquisition and locking. Without a CMS connection,
workflows that use acquire clauses or seedEntity other than global will fail
during the resource discovery phase.
Worker Management
These thresholds control how the engine detects and handles unresponsive workers. The defaults cascade: unreachable (2 min) -> offline (3x = 6 min) -> stuck job (2x offline = 12 min).
| Variable | Default | Description |
|---|---|---|
WORKER_UNREACHABLE_THRESHOLD_MS |
120000 (2 min) |
Time since last heartbeat before worker is marked unreachable |
WORKER_OFFLINE_THRESHOLD_MS |
360000 (6 min) |
Time since last heartbeat before worker is marked offline and its jobs are failed |
WORKER_OFFLINE_DELETE_THRESHOLD_MS |
86400000 (24 h) |
Time before an offline worker is soft-deleted |
WORKER_CLEANUP_ENABLED |
true |
Enable automatic deletion of stale workers |
WORKER_CLEANUP_INTERVAL_MS |
3600000 (1 h) |
How often to check for stale workers |
Workflow Engine
The engine is a single-writer design: one instance drives the epoch machine for a given database. On startup it takes a Postgres session-level advisory lock and waits for any outgoing instance to release it, so a rolling deploy hands over without operator action. If the lock is still held when the timeout expires, the process refuses to start rather than running a second engine against the same database.
| Variable | Default | Description |
|---|---|---|
ENGINE_INSTANCE_LOCK_TIMEOUT_MS |
60000 (60 s) |
How long startup waits for another instance to release the single-instance lock before failing |
EPOCH_MAX_PARALLEL_RUNS |
unset (no cap) | Soft cap on how many workflows an epoch locks before it stops accepting new locks |
EPOCH_RETENTION_DAYS |
30 |
Age after which completed epochs are pruned; 0 or negative disables pruning |
EPOCH_CLEANUP_INTERVAL_MS |
3600000 (1 h) |
How often the epoch retention sweep runs |
Never run two engines against one database
Scale the engine vertically, not horizontally. A second instance is refused by the lock; if you deliberately need two engines, give each its own database.
EPOCH_MAX_PARALLEL_RUNS is a soft cap
It bounds sustained concurrency, not each individual epoch. The coordinator checks the cap between events against a count that may already be stale, while the packing itself happens inside concurrent handler transactions that do not consult it — so a burst of workflows reaching lock acquisition together can overshoot (three workflows packed with the cap set to one has been observed). Leave it unset unless you need to throttle sustained load; do not rely on it as a hard “at most N locked at once” guarantee.
Completed epochs are pruned in batches on the EPOCH_CLEANUP_INTERVAL_MS schedule.
The latest epoch is never deleted, and neither is any epoch a non-terminal workflow
execution is still parked in — so pruning can never disturb work in flight, however
aggressive the retention setting.
Blackboard (Job Management)
| Variable | Default | Description |
|---|---|---|
BLACKBOARD_JOB_CHECK_INTERVAL |
60000 (1 min) |
How often the engine runs its job cleanup cycle (offline workers, stuck jobs, never-polled jobs) |
BLACKBOARD_STUCK_JOB_TIMEOUT |
720000 (12 min) |
Jobs in POLLED state longer than this are auto-failed (default: 2× WORKER_OFFLINE_THRESHOLD_MS) |
BLACKBOARD_PENDING_JOB_TIMEOUT |
3600000 (60 min) |
Jobs in PENDING state that no worker ever polled within this deadline are auto-failed, so a workflow — and the epoch holding it — cannot wait forever for a worker that never comes (default: 5× BLACKBOARD_STUCK_JOB_TIMEOUT). Applies to acquire, execute and rollback jobs alike; a job claimed by a worker while the cleanup runs is left to that worker |
Example .env
# Production configuration
NODE_ENV=production
LOG_LEVEL=info
LOG_FORMAT=json
# Database
POSTGRES_HOST=postgres.internal
POSTGRES_PORT=5432
POSTGRES_USER=neops_engine
POSTGRES_PASSWORD=<secure-password>
# CMS
NEOPS_CMS_URL=http://neops-cms.internal:8000/graphql
NEOPS_CMS_TOKEN=<cms-auth-token>

