Skip to content

Project config (noemata.json)

noemata.json at the workspace root configures the project’s database, UI, telemetry, and file store. The setup wizard (noemata init or noemata config) writes it. You can also edit it by hand. noemata up applies the settings to the services it starts.

Every authored field is snake_case. All top-level fields are optional; the wizard fills in an empty {}. Ports default to "auto", which adds a worktree-specific offset to each standard port. If another process already uses a port, the wizard and noemata up can shift the managed service ports together and write the resulting port numbers here.

This page is a map of the top-level shape. The exhaustive, always-current field list lives in the generated JSON Schema, which editors use for autocomplete and validation — see $schema below.

Top-level shape

{
"$schema": "./.data/schemas/project.json",
"db": { "clickhouse": { "binary": {} } },
"app": { "local": { "port": "auto", "tls": "self-signed" } },
"collect": { "collector": { "binary": {} } },
"integrations": { "installed": {} },
"fs": { "local": {} },
"agents": { "claude_code": {} },
"autostart": {}
}
FieldPurpose
$schemaRelative path to the generated JSON Schema (see below). Auto-managed.
dbThe database Noemata reads and writes.
appHow the Noemata UI is served.
collectThe bundled OpenTelemetry collector.
integrationsThe integrations the project installs and enables.
fsThe file-store topology.
agentsPer-agent settings.
autostartStart this project’s services at login (macOS).
workflowsWorkflow scheduling, delivery, and storage policy.
workersServer workflow pools and per-tab browser SQL pools.
telemetryWhere and how the server exports its own logs, metrics, and traces.

$schema

Relative path to the generated JSON Schema at .data/schemas/project.json. Editors use the schema for autocomplete and validation. The wizard writes and removes this field when it reads the config; do not author it by hand.

db

The database. Today the only backend is ClickHouse, under db.clickhouse, and you pick exactly one of two shapes:

  • binary — a managed local ClickHouse that Noemata downloads and runs (version?, http_port?, native_port?, keeper_port?, keeper_raft_port?, max_memory_gb?, data_dir?). An fs.remote project uses embedded single-node ClickHouse Keeper on client port 9181 and Raft port 9250. Both ports use the worktree offset. The generated server config enables ClickHouse Keeper coordination. An fs.local project stores coordination on disk and does not run Keeper.
  • external — a ClickHouse you already run, including ClickHouse Cloud (hosts?, http_port?, native_port?); up connects but never manages it.

Both share authentication? ("none" — the default — or "password"), default_database? (default "default"), namespace? (the prefix of every table Noemata creates for itself, which keeps deployments that share a database apart, default "noemata"), request_timeout? (query timeout in seconds, default 60 — bounds the client request and the default server-side max_execution_time), query_settings? (a passthrough map of ClickHouse settings for every dashboard query), max_server_connections? (how many queries the server keeps in flight at once, across all clients; unset means no cap), and max_client_connections? (how many one browser tab keeps in flight to the server; unset means no cap). Credentials never live in the file — see The .env file. See Connect a database for the full treatment.

app

How the UI server is served, as one of:

  • local — served on this host. port (required) is a literal number or "auto" (resolved per worktree off 3210); optional tls is "mkcert" (browser-trusted), "self-signed" (encrypted with a browser warning), or "none" (plain HTTP).
  • external — a UI hosted elsewhere (hosts).

local writes api.server to config/server/server.yml. The server applies config/server/server.overrides.yml and NOEMATA_* environment variables over that value. For example, NOEMATA_API_SERVER_PORT=4000 sets the bound port; app.local.port no longer applies. up checks that port for conflicts and reports its configured source when another process uses it. Port shifting only changes ports managed by this file.

collect

The bundled OTel collector, under collector: binary (a managed local collector, with optional version, http_port, grpc_port) or external (hosts).

integrations

Which integrations reconcile into the project, and where they come from:

  • packages lists the npm packages that contribute integrations, by package name, each with its options. trusted (default false) allows the package to write outside the repository; see Trusting a package. The integrations Noemata ships need no entry; listing their package, @noemata/integrations, replaces the copy built into the CLI with the installed one, which stays trusted. A file that still nests integrations under collect is refused with a message naming the move.
  • installed lists the enabled integrations by id: a short id such as claude_code for an integration Noemata ships, or the package name for one installed from npm (see Installing integrations from npm). noemata and opentelemetry are required and reconcile whether or not they appear here.
  • managed (default true) sets whether Noemata owns the installed frame packs — reinstalled and gitignored — or you do, seeded once and version-controlled.
  • skills selects which detected editors receive the integrations’ skill packs: true (the default) installs to all, false to none, or selected hosts (one selector or an array). Skills install at the repository root. This setting controls them; external_edits controls writes outside the repository. Setup asks before installing skills above the project directory and records a decline as false.
  • external_edits (default true) controls whether reconciliation writes files outside the repository, such as an app’s telemetry config at ~/.claude/settings.json. If the project is not in a git repository, or the repository is your home directory or above it, the project directory is the boundary. Setup asks before the first such write and records a decline as false.
  • remove_orphans (default false) lets reconciliation also delete pack files left behind by integrations that are no longer enabled.
  • Per-integration settings key in by integration id under installed — for example claude_code’s redact_content and hooks.validate_frames_on_edit, see Claude Code. An npm package declares its settings as a JSON Schema, and reconcile validates the entry against it.

fs

Where the deployment and the drafts are stored. Pick one:

  • local — the server reads and writes the project directory directly and never writes to ClickHouse. The server must run on the same host. noemata init writes this mode for a new project, and it is the default when fs is not set.
  • remote — the deployment is in the file store in ClickHouse. Requires Keeper coordination; see Storage modes. The optional database sets the database that contains the file store’s tables (default: db.clickhouse.default_database). The optional disk (default false) makes the project directory the draft, in place of the user’s draft in the store. noemata up --edit sets disk for one run.

The project directory is the directory that contains noemata.json. noemata up passes it to the server as NOEMATA_PROJECT_DIR. A server started without the CLI serves its current directory.

In a checkout of an fs.remote deployment, preview edits on disk with disk: true or noemata up --edit, publish them with noemata publish, and write content authored in the app to disk with noemata fetch.

agents

Per-agent settings, keyed by coding agent (the tool rather than the model) — e.g. claude_code. The CLI copies this block verbatim into the generated server config. Set the whole field to false to record that the panel was offered and declined: setup then stops suggesting an agent, and no agents block reaches the server. Omitting the key entirely means “not decided yet”, so setup offers whichever agent it detects.

The agent panel drives the coding agent installed on the server’s host — claude_code runs the claude binary found on the server’s PATH, so the panel is offered only where that binary exists. Its settings: executable_path (an absolute path, for when the binary is somewhere PATH won’t reach, such as a container image), models / default_model / fast_model (which models the panel offers), and cwd (the directory the agent runs in).

A turn runs the agent’s own file and shell tools on the server host, so enabling the panel is a decision about that host. The browser only hides the panel; the endpoints are gated on the same config. The panel runs in a draft on disk (fs.local, or fs.remote with disk: true). When the draft is in the store there is no directory to run in, so the app does not offer the panel.

Sessions the panel runs are ordinary Claude Code sessions, so with the Claude Code integration installed they show up in its dashboards like any other — the same deployment both drives the agent and observes it.

autostart

Start this project’s services when you log in, instead of running noemata up yourself. Present-and-not-disabled is the switch — "autostart": {} turns it on, and { "enabled": false } keeps the block while turning it off.

Every reconcile aligns the login unit, so this field is the source of truth: enabling it installs a per-user launchd agent under ~/Library/LaunchAgents/, and disabling it — or deleting the block — removes that agent. That happens wherever the config is applied — reconcile, config, init, and the pre-start reconcile in up — so noemata reconcile installs the agent without starting anything. noemata rm removes it, so an unlinked project leaves nothing behind. Pass --no-autostart to leave the unit untouched for a single run.

The unit runs noemata up --detach at login and nothing more; the per-service supervisors it starts are what keep the stack alive, exactly as when you run up by hand. Logs from the login run land in .data/log/autostart.log.

Supported on macOS. On other platforms an enabled autostart warns and is skipped, so a shared noemata.json stays portable.

workflows

enabled defaults to false. Set workflows.enabled: true to start the deployment’s scheduler and run the enabled rules in installed packs. Workflow configuration and overlays control individual rules. The interactive setup prompt defaults to true, unless a previous choice is recorded. For a project that drafts on disk against a deployment in the store (fs.remote.disk), the prompt defaults to false. --yes accepts the setup default. noemata up --edit turns scheduling off for that run. Restart the server after changing this deployment setting.

Workflow state uses schedule_state_retention_days. Immutable state payloads live beside each definition under .shared/workflows/<filename>/<workflow-name>/ and expire through the file store’s TTL or local disk sweep. Keeper-backed current records expire logically on read. Run history uses telemetry retention, independently of file retention. The CLI copies this block into generated server config; see Schema upgrades before deployment.

FieldDefaultPurpose
selectionall under fs: local, non_local under fs: remoteThe workflows the scheduler runs: non_local (the published revision), local (the local workflows of the draft), or all. fs: remote accepts only non_local; see What the scheduler reads.
run_retention_days30Retention for legacy run files. New execution does not create run journals; this setting does not control telemetry history.
max_concurrent_runs4Runs admitted per server instance. Further due occurrences wait for capacity; another instance with capacity may take them.
schedule_state_retention_days90Retention for immutable state payloads and idle occurrence cursors. Expired state starts empty; an expired cursor discards catch-up debt. Successful publication refreshes state.
{
"workflows": { "run_retention_days": 14, "schedule_state_retention_days": 180 }
}

Workflow telemetry delivery

workflows.delivery bounds the in-memory telemetry queue per server scheduler. Emission never waits for queue space or collector delivery. Accepted batches can be lost on a process crash; this queue is not durable.

FieldDefaultPurpose
max_pending_batches64Maximum active and queued emit batches combined. One emit invocation is one batch, including both logs and metrics.
max_pending_bytes67108864 (64 MiB)Maximum IPC plus V8-serialized projection metadata bytes across active and queued batches. Materialized-record submissions count V8-serialized snapshot bytes. This does not bound total process memory, encoding output, or temporary allocations.
overflowdrop_newestDiscard the incoming batch, or use drop_oldest to evict waiting batches until it fits. Active deliveries are never evicted.

A batch larger than the byte limit is dropped without evicting anything. If active delivery prevents a batch fitting, the incoming batch is dropped under either policy. Drops produce warnings and noemata.workflow.delivery.dropped.batches / noemata.workflow.delivery.dropped.records counters, labelled by noemata.delivery.drop.reason. Before projection, record counts are candidate counts (input rows times child projections); child filters or invalid rows may reduce the actual output.

Delivery processes one batch at a time, with log and metric requests within that batch sent concurrently. Shutdown stops admission and drains for up to 10 seconds before cancelling remaining deliveries. Disabling worker threads does not disable these queue limits.

Projection and delivery failures produce warnings with workflow/run identity and increment noemata.workflow.delivery.failures, labelled by noemata.delivery.failure.kind (projection, delivery, or task). Log records retain the trace/span IDs of the submitting workflow even when queued.

workers

Workers are on by default. The server runs supported workflow in-memory SQL in its sql pool, and workflow telemetry projection, OTLP/JSON encoding and delivery in its telemetry pool; each browser tab runs its in-memory SQL in its own pool. Set workers.enabled: false to run everything on the main threads, or workers.server.enabled: false / workers.browser.enabled: false to turn off one platform. Configure kinds under workers.server.kinds and workers.browser.kinds. Unsupported SQL still runs in the database. Restart the server and reload browser tabs after changing pool settings.

{
"workers": {
"server": {
"enabled": true,
"kinds": {
"sql": { "max_threads": 2, "max_queue": 32 },
"telemetry": { "min_threads": 1, "max_threads": 1 }
}
},
"browser": {
"enabled": true,
"inline_below_cells": 10000,
"kinds": { "sql": { "max_threads": 2, "max_queue": 32 } }
}
}
}

Global enabled: false overrides both server and browser pools and runs tasks on their respective main threads. A kind can independently set enabled: false. Browser kinds do not inherit server kind settings. Defaults by kind:

Kindmin_threadsmax_threadsidle_timeout_ms
Server sql04300000
Server telemetry1230000
Browser sql481800000
Any other kind0230000

Every kind defaults to max_queue: 64. An authored max_threads below a kind’s default min_threads lowers the minimum to match; an authored min_threads is kept as written. A pool starts its min_threads when it is created: the server’s telemetry thread at startup, and a tab’s four sql workers as soon as the application script runs, before the app renders. Other threads start when a task needs one. max_old_generation_size_mb is Node-only and limits each worker’s V8 old-generation heap; it does not cap Arrow buffers or total process memory. Browser pools reject this option. An exhausted queue rejects new tasks, except in-memory SQL, which then runs on the main thread. A worker fault during a task rejects that task without a retry. A worker that fails outside a task, for example while starting, stops its kind’s pool: the kind’s accepted and later tasks run on the main thread until the server restarts or the page reloads.

In-memory SQL that works on fewer than inline_below_cells cells runs on the main thread. The cells are the input rows the query touches times the columns it reads. A query that only selects columns touches no rows, so it always runs on the main thread, and a LIMIT ahead of any filter, sort or aggregate counts only the rows it keeps. The browser default is 10,000 cells: dashboard queries work on a few hundred cells and take under a millisecond on the main thread, while a worker’s reply waits until the tab has finished rendering. The server default is 0, which sends every other query to a worker, so each result arrives on a new event-loop turn while workflows run. Set workers.inline_below_cells for both platforms, or workers.browser.inline_below_cells / workers.server.inline_below_cells for one.

Browser pools use dedicated module Web Workers shared by evaluations within one tab. Planning and input Arrow encoding remain on the browser’s main thread. A queued task goes to the first worker that is free; a worker started for a burst of tasks only adds capacity. Cancelling a running task lets its worker finish and drops the result, and a worker still running a cancelled task after one second is terminated and replaced. Cancelling a queued task removes it without transferring its buffers. Idle workers retire down to min_threads. Closing the document aborts the pool; back/forward-cache suspension preserves it. Worker initialization has a 30-second deadline; a bundle-load, CSP, or initialization failure counts as a worker failing outside a task.

The application bundles SQL workers separately under hashed /assets/ URLs. The PWA service worker caches these immutable bundles on first use; it does not execute SQL or own the pool. Deployments with CSP must permit worker-src 'self'. Keep old hashed assets available while older tabs remain open: a tab may need to create another worker after a deployment, before that bundle has entered its cache. No Blob URLs or cross-origin isolation are required.

Pool telemetry reports task duration/outcome, accepted and queued tasks, and thread counts under the @noemata/workers scope on both platforms. The noemata.in_memory_query span records the cells a query works on (noemata.db.in_memory.cells) and whether it ran on the main thread or in a worker (noemata.worker.mode: inline or worker). Node pools additionally expose Piscina’s pool-lifetime wait/run timing summaries. The workflow delivery queue holds its own copy of each batch’s Arrow IPC. An in-memory SQL task keeps a reference to its input tables’ Arrow bytes while it waits, and they are copied when a worker takes the task. Per-row projection and delivery failures are reported asynchronously without failing the workflow. The telemetry worker uses OTLP/HTTP JSON; compression and alternative wire formats are not enabled by this configuration.

telemetry

Where and how the server exports its own logs, metrics, and traces (the OTel signals it produces about itself, not the data it ingests). Keys authored here flow into the generated server.yml and reach the server SDK. Every field is optional; a project that omits the block relies on collect.collector and env overrides.

  • collector.hosts — OTLP endpoint URL (string or array of strings). Points the server’s exporters at your collector, deployed separately from the CLI-managed one. When collect.collector is also set, telemetry.collector.hosts wins — the derivation is only used when the project has not set one.
  • collector.protocol — "http/protobuf" (default), "http/json", or "grpc".
  • environment — emitted as the semconv deployment.environment.name resource attribute (e.g. "production", "demo"). Defaults to production for the published CLI and server, and to development when running from the monorepo source. NOEMATA_TELEMETRY_ENVIRONMENT overrides it. A server or browser override that sets other fields keeps the shared value.
  • role — the deployment’s role, emitted on the server resource as noemata.role: "server" (default) serves the app, "runner" runs the workflow scheduler, "viewer" serves a read-only viewer. The value is a label and changes no behaviour, so a runner still needs workflows.enabled: true. Deployments that run several instances from one config set it per instance with NOEMATA_TELEMETRY_ROLE. The self-monitoring views expose it as Role.
  • attributes — extra resource attributes merged into every signal.
  • server, browser — per-target overrides layered onto the shared block; each accepts the same fields recursively.
  • tracing, logging, metrics, profiling — per-signal switches and processor tuning.

service_version is stamped by the CLI (the CLI is the authority on the version it ships), so a hand-authored value here is dropped. server.overrides.yml and NOEMATA_TELEMETRY_* env variables still win over the generated file.

On Kubernetes, the server adds the pod’s identity to its resource from three environment variables: K8S_POD_NAME sets k8s.pod.name, K8S_NAMESPACE_NAME sets k8s.namespace.name, and K8S_NODE_NAME sets k8s.node.name. Unset variables add nothing, and a key in attributes wins over the variable. Set them from the downward API in the pod spec so the server’s telemetry joins to the pod’s CPU and memory metrics even when the collector does not enrich it:

env:
- name: K8S_POD_NAME
valueFrom: { fieldRef: { fieldPath: metadata.name } }
- name: K8S_NAMESPACE_NAME
valueFrom: { fieldRef: { fieldPath: metadata.namespace } }
- name: K8S_NODE_NAME
valueFrom: { fieldRef: { fieldPath: spec.nodeName } }
{
"telemetry": {
"collector": { "hosts": "http://collector.example:4318" },
"environment": "demo",
"server": { "attributes": { "service.name": "noemata-readonly-demo" } }
}
}

The .env file

Secrets stay out of noemata.json and live in the project’s .env instead:

  • NOEMATA_ENCRYPTION_KEY — encrypts the session cookies that carry database credentials. noemata init, config, up and reconcile each generate one into .env if none exists, and never overwrite one you set (so a key rotated by hand or injected from a secret manager survives every reconcile). A production server refuses to start on the placeholder default, so set a real one in any deployment you don’t run through the CLI.
  • NOEMATA_CH_USERNAME / NOEMATA_CH_PASSWORD — the database credentials used when db.clickhouse.authentication is "password".

The file is gitignored and written owner-only (0600); every reconcile tightens the permissions of one that was created some other way.

The environment wins. A variable already set in the environment takes precedence over the same key in .env, so a CI runner or a secret manager can supply credentials without editing the file, and NOEMATA_CH_PASSWORD=… noemata publish overrides for one command.

Editing it by hand. Quote any value that isn’t plain — anything with a space, a #, or a leading or trailing space — as KEY='value'. Single quotes are taken literally, so they carry #, backslashes and " unchanged. Reconcile preserves your comments and spelling; it rewrites only the keys it manages, and refuses to write a value containing both a ' and a " rather than store something that would read back different (set that one in the environment instead).

See also

Installation identity

The CLI persists a human-readable identifier in .data/instance.json during initialization and passes instance_id in generated server configuration. Preserve that file across restarts and cache cleanup. Keep it outside Git and do not copy it between installations. noemata.lock.json remains a Git-tracked dependency lock. Direct server startup requires an explicit valid instance_id and never creates an identity file. Read-only CLI commands require prior initialization.