Configuration reference¶
Every per-tenant value, where it lives, who can change it, and when a change takes effect. That last column is the one that causes trouble when it is not written down.
Consolidated from the ADRs listed against each row. Where an ADR and this page disagree, the ADR wins.
The three tiers¶
Set by ADR-0010, unchanged since.
flowchart LR
S["<b>Secrets</b><br/>Ansible Vault<br/><i>per-tenant vault-id</i>"] --> H["Rendered to the host<br/>at deploy time"]
I["<b>Infrastructure config</b><br/>tenants/{slug}/vars.yml<br/><i>git, reviewable</i>"] --> H
B["<b>Business config</b><br/>Registry<br/><i>edited in the Console</i>"] --> R["Read by the brain<br/>per request"]
| Tier | Lives in | Changed by | Takes effect |
|---|---|---|---|
| Secrets | Ansible Vault, per-tenant --vault-id |
A PR, then a playbook run | On redeploy |
| Infrastructure | infra/tenants/{slug}/vars.yml |
A PR, then a playbook run | On redeploy |
| Business | Registry | The BOS Console | New work items only |
The bottom row is the important one. Business config never applies retroactively to in-flight work items, because each item is pinned to the config version in force when it was created (ADR-0006). Otherwise nobody could explain, six months later, why an old item behaved differently.
Identity and placement¶
| Value | Example | Tier | Effect | Notes |
|---|---|---|---|---|
tenant_slug |
greenhills |
Infra | Immutable | Appears in DNS, certificates, database names, vault IDs, directory and backup paths. Does not change if the client rebrands (ADR-0022 §7) |
region |
au-mel |
Infra + registry | Redeploy | AU residency is hard (ADR-0008). Checked by the provisioning pre-flight gate against inventory, and a mismatch fails closed (ADR-0024 §6) |
ansible_host |
Tailscale address | Infra | Redeploy | Points at the physical box. Moving a tenant to dedicated hardware is this one line (ADR-0024 §1) |
| Vanity domain | portal.client.com.au |
Infra | Redeploy | CNAME, never a redirect. Caddy config first, DNS second (ADR-0022 §5) |
What is deployed¶
| Value | Options | Tier | Effect | Notes |
|---|---|---|---|---|
services |
[core, n8n] or [core, n8n, metabase] |
Infra | Redeploy | Metabase only for clients who bought the dashboard product. Around 2 GB each (ADR-0023 §5) |
langfuse_enabled |
true or false |
Infra | Redeploy | Host-level, not per tenant. One shared Langfuse with a project per tenant. Around 6 to 8 GB for six containers (web, worker, ClickHouse, MinIO, Redis, Postgres), which is the largest single line on the box. Off by default and not deployed for client-zero. The role exists, so turning it on is config plus a playbook run. Losing it costs nothing permanent, because the durable record is Postgres |
langfuse_retention_days |
30 |
Infra | Next prune | Traces are for debugging the last few days. Retention also bounds residency exposure |
| Resource limits | Per container | Infra | Redeploy | Check the sizing worksheet before adding a dashboard tenant to a loaded host |
Behaviour¶
| Value | Options | Tier | Effect | Notes |
|---|---|---|---|---|
approval_surface |
review_app or mailbox |
Business | New items only | One mode, never both (ADR-0032 §6h). Defaults to mailbox: the draft appears in their own mail client, where attachments are native and the tool is one they already know. In-flight items finish under the mode they started in |
scratch_reply_surface |
review_app or mailbox |
Business | New items only | Where a human writes a reply for an item that has no draft. Writing from scratch is editing an empty draft, so the editor already exists either way. Defaults to mailbox, because attachments are the common need and drag-and-drop beats an upload flow |
scheduling_mode |
none, propose or hold |
Business | New items only | Whether the draft may offer times, per ADR-0035. Defaults to propose, falling back to none where the connector cannot answer a find_slots question. Selecting a mode the connector cannot support is refused by the Console, never silently downgraded. hold additionally requires a genuine tentative_hold capability and a proven release job |
scheduling_constraints |
Structured predicates | Business | New items only | Tenant scheduling rules using the same {fact, operator, value} shape as everything else (ADR-0004): working hours, day exclusions, travel buffers, skill-to-resource mappings, lead time. Config, not code, so a client's preferences never require a deploy |
hold_expiry |
Duration | Business | New items only | How long a tentative hold survives before release. Two to three days typical. Only meaningful under hold. The release job must run reliably forever: if it fails silently, their calendar fills with phantom holds and nobody notices until a week looks fully booked and is not |
store_human_replies |
true or false |
Business | New items only | Whether we keep the body of a reply a human wrote themselves. Off by default: a client opts in to us keeping their staff's writing. Detection is never optional, since reconciliation needs the Message-ID to avoid sending a duplicate. Off means the Draft prompt improves more slowly for that client, because a reply a human wrote from scratch is the richest signal available |
multi_category_handling |
escalate_all or resolve_then_decide |
Business | New items only | What happens when one message carries two or more categories. escalate_all is the v1 default and the only behaviour built. resolve_then_decide runs the lookups first and drafts a single reply if nothing is left outstanding. Whether one action closes both cannot be known at classification time, since it depends on what the lookups find |
category_selection |
first_mentioned or by_rank |
Business | New items only | Which of several categories becomes primary. The model does not choose: it reports every category present in the order they appear, and code applies this policy. by_rank is the default because the primary is what gets flagged to a human, so the more consequential ask should win |
category_rank |
Ordered list | Business | New items only | Ranking used by by_rank, defaulting from the vertical template. Not the same as priority, which is urgency on the work item. Rank only decides which category is primary |
| Enabled catalogue actions | Subset of the catalogue | Business | New items only | The brain cannot propose an action a tenant has not switched on (ADR-0032 §6f) |
| Autonomy level | assist |
Business | New items only | Assist is currently the only defensible setting: there is no eval harness to justify more (OI-060) |
actions_requiring_approval |
Subset of the enabled actions | Business | New items only | The autonomy dial, per action. Defaults to every action at onboarding, so a client sees each one before it acts unsupervised. Relaxed action by action as they gain confidence: typically close_item first, since a wrongly closed item is recoverable and visible in history. Config may only add to the catalogue's floor, never remove from it (see below) |
acknowledgement_enabled |
true or false |
Business | New items only | Whether draft_acknowledgement may be proposed. Off by default. An acknowledgement from a firm that always answers within the hour looks like padding. From one where things sit for two days it is the single biggest improvement available, because response time is what customers complain about. When false, an item that would have been downgraded gets no draft at all. Never sent alongside a substantive draft, and it makes no model call |
acknowledgement_template |
Free text | Business | New items only | What the acknowledgement says. Kept short, and it promises a timeframe the client can actually meet |
acknowledgement_closes_item |
true or false |
Business | New items only | Whether sending an acknowledgement closes the work item. Some clients treat it as "we have it, we will handle this offline" and the item's job is done. Others want it held open until a real answer goes out. Defaults to false, because closing an item whose customer never got a real answer is the failure to avoid |
close_review |
required, notify or none |
Business | New items only | What happens when the brain proposes close_item. required: nothing closes until a human confirms, and the correction is a labelled example for the eval set. notify: closes immediately and appears in a "closed today" list. none: closes silently, visible in history. The middle rung matters, because going from required straight to none loses sight of it entirely |
max_brain_calls |
Integer, low | Business | New items only | Halts an item that is looping and flags it. Counts brain calls, not model calls, since a call that hits the idempotency check costs nothing. A cycling agent is a cost failure and a correctness failure at once (ADR-0032 §3) |
claim_timeout |
Duration | Business | New items only | How long an item stays claimed by whoever opened it. Minutes, not hours. Someone else may read a claimed item but not act on it, and an Owner may force-release, which is recorded. Prevents two people in a shared queue both approving the same draft |
| Mailbox roles | owner, approver, assistant |
Business | Immediate | Permission attaches to the mailbox, not the tenant, since a business may run several with different people on each. Owner: everything, including flagged items and config. Approver: routine drafts only, never a flagged one. Assistant: confirm closes and clear the confirmation queue, but nothing that sends. The Owner and Approver boundary is attention_reason being null or set, so it is one field rather than a permission matrix |
max_calls_since_human |
Integer | Business | New items only | Regenerations allowed before an item is flagged and regeneration stops. Counts calls since the last human action, not calls in total, because a total separates none of the three cases: a loop, an abandoned item and a legitimate six-week negotiation all show high totals. Counting since the last human action means a real conversation resets constantly and never approaches the threshold |
draft_staleness |
Duration | Business | New items only | How long a waiting draft may sit before it is regenerated. A draft written on Monday and approved on Thursday goes out saying things that were true on Monday, and ageing does not fix that. Same mechanism as a chase, triggered by time rather than by the customer. Floor 24 hours (below that you regenerate faster than people check a queue), recommended 48 to 72 hours, ceiling 7 days. Every regeneration is two model calls, so a one-day threshold on an item sitting a week costs fourteen. Approvals do not expire: ageing handles visibility, staleness handles content, and nothing auto-approves |
priority_age_interval |
Duration | Business | New items only | How long an item waits in the action queue before its effective priority escalates one level. Computed at read time, so stored priority keeps meaning "how urgent was this message" rather than drifting into "how long has it sat there" |
priority_age_cap |
Priority level | Business | New items only | Ceiling for age escalation. Defaults to one below urgent, so a genuinely urgent item is never buried under an old trivial one |
chase_bump |
Integer | Business | New items only | Levels added when a customer sends another message on an item already awaiting_approval. A chase is evidence rather than inference, so it does not decay. It also invalidates the draft sitting there, which was written before the follow-up arrived |
| Rules and thresholds | Structured predicates | Business | New items only | {fact, operator, value} over the published catalogue. No expression language, no eval (ADR-0004). Precedence: guardrails, then vertical template, then tenant authored, then learned (ADR-0005) |
| Taxonomy and categories | Per client | Business | New items only | Signed off by the client before the first run. Redefining a category in month four silently invalidates months one to three (OI-008) |
reply_voice |
Free text | Business | New items only | Formality, sign-off, standing instructions. Used by the Draft prompt |
reply_html_template |
HTML | Business | New items only | The wrapper a drafted body is rendered into. The model returns text and never authors HTML, which avoids broken markup and keeps branding consistent. Rendered by the brain, since only it reads the registry |
reply_signature |
HTML | Business | New items only | Appended by the template, so it is never something the model has to remember or get right |
| Tenant mode | Per ADR-0013 | Business | Varies | Pilot and demo modes carry expiry semantics |
Alerting¶
Alert definitions are config, and the mechanism is ADR-0034.
An alert is a rule over aggregates, reusing the {metric, operator, value} shape from ADR-0004 but evaluated on a schedule rather than per work item.
| Field | Options | Tier | Notes |
|---|---|---|---|
metric |
From the published catalogue | Business | e.g. abandoned_rate, parse_failure_rate |
operator, threshold |
Business | Count or percentage is just a choice of metric, not a special case | |
window |
Duration | Business | Rolling period the metric is computed over |
scope |
tenant or global |
Business | Global is what justifies the mechanism: one tenant's parse failures could be their prompt, and every tenant's at once is a provider-side model change no per-tenant view would catch |
destination |
Console, email, Teams | Business | |
kind |
incident or advisory |
Business | Must be designed in, not added later. Every Console alert so far means "broken, fix today". An abandonment alert means "ring the client". On a shared surface the advisory gets triaged as a bug and dismissed |
cooldown |
Duration | Business | Mandatory. A condition true for a week notifies once. Alert fatigue kills monitoring faster than missing alerts do |
| Client-visible | true or false |
Business | Off by default. Allowed, and not until the alert set is proven |
Platform monitoring is not built on this (ADR-0034 §3). Host metrics have no tenant dimension and a language covering both would serve neither.
A default set ships enabled. A configurable system nobody configures is worse than a fixed one.
Backups¶
| Value | Default | Tier | Effect | Notes |
|---|---|---|---|---|
| Backup schedule | 02:30 daily | Business | Next run | Per-tenant, so a client with a compliance obligation is accommodated without a code change (ADR-0026 §6) |
| Daily retention | 30 | Business | Next prune | |
| Monthly retention | 12 | Business | Next prune | |
| Approval log retention | Floor applies | Business | Next prune | Longer on request, never shorter. Shortening it discards our own defence, and the client is not the party it primarily protects. A request to reduce it is a conversation, not a config change (ADR-0026 §6) |
Reporting¶
| Value | Tier | Effect | Notes |
|---|---|---|---|
| Metric definitions | Business | New items only | Client-signed-off before the first run (OI-008) |
| Minutes-saved multiplier | Business | Next report | A model, not a fact. Agreed at Blueprint and recorded, so a challenge is answered with their sign-off rather than our assumption (OI-009) |
| Sync cadence | Business | Next sync | Get the client to state a number. 15 minutes is almost certainly enough and costs a fraction of the AroFlo API budget (OI-005) |
Rules that hold across all of it¶
Config, not code. New behaviour is config in the control plane, never a per-client fork. If it genuinely needs new logic, the shared vocabulary is extended so every tenant benefits, then switched on as config (ADR-0003).
The control plane holds the map, never client content. Tenant records, resolved config, connector manifests, metadata. No emails, no invoices, no client data (ADR-0006).
Two writers only. Ansible creates the tenant record at bootstrap, and the Console edits config thereafter. The brain reads and never writes.
Business config never applies retroactively. Each work item is pinned at creation to the config and catalogue versions then in force.
The test that matters: could this tenant be rebuilt from the repo and its config rows alone? If not, that is drift, and drift turns ten clients into ten products.
System settings¶
Application settings that apply to every tenant, so no client has a view on them. They sit in the infrastructure tier, meaning environment variables templated by Ansible, and changing one is a redeploy.
The test for whether something belongs here rather than in business config: would a client ever reasonably ask for a different value? If the answer is no, it is a system setting.
Model calls¶
| Setting | Default | Notes |
|---|---|---|
llm_max_attempts |
3 |
First attempt plus two retries |
llm_backoff_base_seconds |
1 |
Exponential, so roughly 1s then 4s |
llm_request_timeout_seconds |
60 |
Per attempt, not total |
What is retried matters more than how often. Retrying the wrong class of failure is worse than not retrying at all.
| Response | Retry? |
|---|---|
| Timeout, connection reset, 5xx | Yes. Transient |
| 429 rate limited | Yes, and honour the Retry-After header rather than guessing |
| 400 bad request | No. It will fail again, more expensively |
| 401 or 403 | No. A credential problem does not fix itself |
Honouring Retry-After is the one most often missed. The provider is telling you when to come back, and guessing instead either wastes time or hammers a service that has just asked you to stop.
Brain API¶
| Setting | Value | Notes |
|---|---|---|
brain_request_timeout_seconds |
Derived, not set | n8n's timeout on a brain call |
Derived, because two hand-set numbers drift
n8n's timeout must exceed the worst case the brain can legitimately take. Set it lower and n8n gives up while the brain is still working, then retries a turn already in progress.
So it is computed, never typed:
With the current defaults: (3 × 60) + 5 + 30 = 215 seconds.
Enforced in two places:
| Where | What |
|---|---|
| Ansible, at provisioning | The n8n timeout is templated from the brain settings, so both come from one source |
| The brain, at startup | Asserts the relationship holds, as a backstop for anyone editing config on the box |
The failure this prevents produces duplicate work rather than an error, which is why it is worth designing out. The idempotency key catches it (ADR-0032 §6c), but relying on a safety net for something a template can guarantee is the wrong trade.
Approval floors¶
actions_requiring_approval is the only config value with a floor set in code.
| Set by | May a tenant change it? | |
|---|---|---|
Catalogue requires_approval |
The action definition | No. It is a floor |
actions_requiring_approval |
Tenant config | Yes, but only to add actions |
Some actions require approval permanently, whatever a client asks: draft_reply and create_job both change something outside our control and cannot become unsupervised (ADR-0032 §6b). Others default to requiring approval during onboarding and may be relaxed later.
Config can only ever make approval more required, never less. A tenant asking to remove approval from draft_reply is a conversation about the "no review, no go" commitment, not a config change, and the Console must refuse it rather than accepting and silently ignoring it.
This mirrors the rule tiering: a lower tier never widens what a higher one narrowed (ADR-0005).
Open¶
Gaps identified while writing this page are tracked in the register, not here: