Configuration
In one line: every environment variable, feature flag, secret, and default — and the env-drift trap where
env.exampleis both stale and missing 23 keys the code actually reads.
What it is
Everything tunable is an env() key set through lua env <environment> (the live agent reads via env(), not process.env — process.env appears only in one-off src/scripts/*). The big levers:
- BC OAuth credentials and
BC_ENVIRONMENT(sandbox vs prod) APPROVER_EMAIL— the single most overloaded knob (admin identity + escalation recipient + brief recipient)LUA_EMAIL_CHANNEL_ID— the outbound mailbox identity- Two feature flags:
SLOT_SCHEDULING_ENABLED,BC_AUTO_POST_INVOICE - Reminder-delay and timeout hours,
APPROVAL_THRESHOLD,VISIT_TIMEZONE, and the brief*_FORCEtest switches
Some thresholds (stale-ticket days, quote-expiry days, cost-variance %, min completion photos) come from BC's maintenanceSetup table, not env — with env overriding for APPROVAL_THRESHOLD and MIN_COMPLETION_PHOTOS.
⚠️ Read this first:
env.exampleis stale and unsafe — it documents 4 unused keys, omits 23 keys the code reads (including secret-grade ones), and contains real-looking BC credential values. A new dev copying it to.envgets a non-functional, mis-configured agent. See Env drift.
Reference — configuration
Defaults shown are the code fallback (used if the env key is unset). "Read at" is a representative read site.
Secrets / credentials
| Var | Purpose | Default | Read at | Effect when changed |
|---|---|---|---|---|
BC_TENANT_ID | BC OAuth tenant (Azure AD) | 6a9ec201-… | bc-auth.ts:21 | Points all BC calls at a different AAD tenant |
BC_CLIENT_ID | BC OAuth client/app ID | 39d3dbfd-… | bc-auth.ts:22 | Changes the BC app identity |
BC_CLIENT_SECRET | BC OAuth client secret | hardcoded literal | bc-auth.ts:23 | Auth to BC. ⚠️ sandbox/local value is your-client-secret-here → 401 on every call |
APPROVAL_TOKEN_SECRET | HMAC key for approval tokens | none — throws if unset | approval-token.ts:32 | Signs/verifies approval tokens (largely legacy — POST-only webhooks) |
LUA_API_KEY | Auth for Lua send-message (outbound email) | none — throws | email-notifications.ts:22 | Required for ALL outbound email; unset ⇒ no sends |
AGENTMAIL_API_KEY | AgentMail REST key | none — throws | agentmail.ts:21 | Auth for the AgentMail client |
FINANCE_API_KEY | Bearer for optional finance API | unset ⇒ skipped | SendForApprovalTool.ts:173 | With FINANCE_API_URL, enables external finance posting |
Feature flags / switches
| Var | Purpose | Default | Read at | Effect when changed |
|---|---|---|---|---|
SLOT_SCHEDULING_ENABLED | Master kill-switch for slot scheduling | ON unless literally 'false' | slot-config.ts:15 | 'false' reverts to legacy single-time + no-ops the slot-timeout job |
BC_AUTO_POST_INVOICE | Auto-post the BC purchase invoice on completion | OFF (only 'true' enables) | CompleteJobTool.ts:158 | 'true' posts the invoice automatically; else PO left open for manual posting |
APPROVAL_THRESHOLD | €-threshold for finance approval; also turns auto-approval ON | unset ⇒ BC maintenanceSetup; default 500 | bc-custom-api.ts:789 | Set >0 ⇒ env override wins AND auto-approval enabled at that threshold |
DAILY_BRIEF_FORCE | Bypass the once-per-day brief guard | unset (guard active) | daily-report.job.ts:59 | '1' re-sends today's brief on manual trigger |
WEEKLY_BRIEF_FORCE | Bypass the once-per-week guard | unset (guard active) | weekly-report.job.ts:32 | '1' bypasses guard + release-on-failure |
Tuning / thresholds
| Var | Purpose | Default | Read at |
|---|---|---|---|
SLOT_PICK_TIMEOUT_HOURS | No-pick auto-cancel window | 12 | slot-config.ts:21 |
TENANT_FEEDBACK_DELAY_HOURS | Hours after completion → tenant feedback email | 24 | feedback-config.ts:32 |
VENDOR_COMPLETION_REMINDER_DELAY_HOURS | Hours after visit → vendor proof chase | 24 | feedback-config.ts:33 |
MIN_COMPLETION_PHOTOS | Min completion photos | unset ⇒ BC setup else 1 | bc-custom-api.ts:801 |
AUTO_REPLY_RATE_LIMIT | Max auto-replies per sender per window | 40 | reply-rate-limit.ts:31 |
AUTO_REPLY_RATE_WINDOW_MIN | Rate-limit window (min) | 10 | reply-rate-limit.ts:36 |
DAILY_BRIEF_MAX_LOG_ROWS | Cap rows in daily-brief log table | unset | daily-report.job.ts:378 |
WEEKLY_BRIEF_MAX_LOG_ROWS | Same cap, weekly | unset | weekly-report.job.ts:73 |
Identity / routing
| Var | Purpose | Default | Read at | Effect when changed |
|---|---|---|---|---|
APPROVER_EMAIL | THE admin identity (comma-separated) — flags user.isAdmin; escalation + daily/weekly brief recipient; approval recipient | unset ⇒ falls back to BC approvalEmail1/2/3 | admin-mode.preprocessor.ts:42; admin-emails.ts:30 | Adding an email makes that sender an admin AND routes briefs/escalations there. @heylua.ai entries dropped (loop guard). Set per-environment via the APPROVER_EMAIL env var (value not shown). |
ADMIN_DEFAULT_COMPANY_ID | Default BC company for admin tools + briefs | TRIBECA_COMPANY_ID | daily-report.job.ts:81 | Re-scopes admin queries and both briefs |
LUA_AGENT_ID | Agent ID for send-message | none — throws | email-notifications.ts:16 | Identifies the sending agent; unset ⇒ no email |
LUA_EMAIL_CHANNEL_ID | Outbound email channel UUID | unset ⇒ default chat@heylua.ai | email-notifications.ts:108 | Set to send from the branded property@… mailbox |
BC_ENVIRONMENT | BC environment (sandbox vs prod) | 'Production' | bc-client.ts:20 | Switches BC target. Also gates test-user overrides (forced empty in Production) |
AGENT_REPLY_TO_EMAIL | Reply-To on approval emails | property@highfieldproperty.ie | SendForApprovalTool.ts:256 | Changes where approvers reply |
EMAIL_THREAD_DOMAIN | Domain for synthetic Message-ID roots | unset | email-thread-anchor.ts:183 | Sets the domain in thread anchors |
MANAGER_USER_ID | Optional in-app daily-report ping | null | daily-report.job.ts:42 | If set, daily report also sent in-app |
VISIT_TIMEZONE | IANA zone for visit-date math + brief schedules/labels | 'Europe/London' | agent-timezone.ts:17 | Changes "today/tomorrow/next Friday" math + brief fire-time. Invalid → falls back to London |
AGENTMAIL_INBOX_ID | Pre-configured AgentMail inbox | unset ⇒ auto-create | agentmail.ts:61 | Pins the AgentMail inbox |
AGENTMAIL_OVERRIDE_TO | Force ALL AgentMail outbound to one address | stefan@heylua.ai | agentmail.ts:27 | Every AgentMail send rewritten here |
TEST_USER_PHONE / TEST_USER_EMAIL | Local-testing identity override | unset; forced empty in prod | user-context.preprocessor.ts:56-57 | Impersonates a BC customer/vendor for sandbox testing |
FINANCE_API_URL | Optional finance API base URL | unset ⇒ skipped | SendForApprovalTool.ts:172 | With FINANCE_API_KEY, enables finance posting |
Non-env constants (in source, not env)
DEFAULT_APPROVAL_THRESHOLD=500 (constants.ts:146); SLA hours — emergency 1, high 4, medium 24, low 72, approval-escalation 24, vendor-response 48 (constants.ts:165-172); BC_REPAIRS_ACCOUNT='25105'; TRIBECA_COMPANY_ID. BC maintenanceSetup-sourced (runtime): staleTicketDays (30), quoteExpiryDays (14), costVarianceAlertPct (150).
Env drift (code vs env.example)
In env.example but NEVER read by live code: VENDOR_API_URL, VENDOR_API_KEY, STRIPE_SECRET_KEY, OPENAI_API_KEY (dead/aspirational). Plus real-looking committed BC credential values (hygiene risk).
Read in code but ABSENT from env.example (the bigger trap): APPROVER_EMAIL, ADMIN_DEFAULT_COMPANY_ID, APPROVAL_TOKEN_SECRET, LUA_EMAIL_CHANNEL_ID, AGENT_REPLY_TO_EMAIL, EMAIL_THREAD_DOMAIN, AGENTMAIL_API_KEY, AGENTMAIL_INBOX_ID, AGENTMAIL_OVERRIDE_TO, SLOT_SCHEDULING_ENABLED, SLOT_PICK_TIMEOUT_HOURS, BC_AUTO_POST_INVOICE, MIN_COMPLETION_PHOTOS, TENANT_FEEDBACK_DELAY_HOURS, VENDOR_COMPLETION_REMINDER_DELAY_HOURS, AUTO_REPLY_RATE_LIMIT, AUTO_REPLY_RATE_WINDOW_MIN, DAILY_BRIEF_FORCE, DAILY_BRIEF_MAX_LOG_ROWS, WEEKLY_BRIEF_FORCE, WEEKLY_BRIEF_MAX_LOG_ROWS, VISIT_TIMEZONE.
In both: LUA_API_KEY, LUA_AGENT_ID, BC_TENANT_ID, BC_CLIENT_ID, BC_CLIENT_SECRET, BC_ENVIRONMENT, APPROVAL_THRESHOLD, MANAGER_USER_ID, TEST_USER_PHONE, TEST_USER_EMAIL.
Gotchas & failure modes
APPROVER_EMAILis triple-duty. It controls who is an admin (unlocks admin tools, bypasses "not registered"), escalation recipients, AND both brief recipients. One typo cuts off admin access and silences briefs/escalations.SLOT_SCHEDULING_ENABLEDis "on unless exactly'false'." A typo or empty value leaves it ON.APPROVAL_THRESHOLDis overloaded. Setting it >0 also flips auto-approval ON and makes env win over the BC value.- BC secret is a placeholder outside prod → 401 on every BC call; nothing BC-dependent is testable end-to-end offline. A real-looking secret is also hardcoded as the
bc-auth.ts:23fallback (flag for rotation). - Two separate outbound email lanes (
LUA_*vsAGENTMAIL_*) — a brief not arriving is a Lua-channel issue, not AgentMail.