Skip to main content

Configuration

In one line: every environment variable, feature flag, secret, and default — and the env-drift trap where env.example is 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.envprocess.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 *_FORCE test 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.example is 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 .env gets 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

VarPurposeDefaultRead atEffect when changed
BC_TENANT_IDBC OAuth tenant (Azure AD)6a9ec201-…bc-auth.ts:21Points all BC calls at a different AAD tenant
BC_CLIENT_IDBC OAuth client/app ID39d3dbfd-…bc-auth.ts:22Changes the BC app identity
BC_CLIENT_SECRETBC OAuth client secrethardcoded literalbc-auth.ts:23Auth to BC. ⚠️ sandbox/local value is your-client-secret-here → 401 on every call
APPROVAL_TOKEN_SECRETHMAC key for approval tokensnone — throws if unsetapproval-token.ts:32Signs/verifies approval tokens (largely legacy — POST-only webhooks)
LUA_API_KEYAuth for Lua send-message (outbound email)none — throwsemail-notifications.ts:22Required for ALL outbound email; unset ⇒ no sends
AGENTMAIL_API_KEYAgentMail REST keynone — throwsagentmail.ts:21Auth for the AgentMail client
FINANCE_API_KEYBearer for optional finance APIunset ⇒ skippedSendForApprovalTool.ts:173With FINANCE_API_URL, enables external finance posting

Feature flags / switches

VarPurposeDefaultRead atEffect when changed
SLOT_SCHEDULING_ENABLEDMaster kill-switch for slot schedulingON unless literally 'false'slot-config.ts:15'false' reverts to legacy single-time + no-ops the slot-timeout job
BC_AUTO_POST_INVOICEAuto-post the BC purchase invoice on completionOFF (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 ONunset ⇒ BC maintenanceSetup; default 500bc-custom-api.ts:789Set >0 ⇒ env override wins AND auto-approval enabled at that threshold
DAILY_BRIEF_FORCEBypass the once-per-day brief guardunset (guard active)daily-report.job.ts:59'1' re-sends today's brief on manual trigger
WEEKLY_BRIEF_FORCEBypass the once-per-week guardunset (guard active)weekly-report.job.ts:32'1' bypasses guard + release-on-failure

Tuning / thresholds

VarPurposeDefaultRead at
SLOT_PICK_TIMEOUT_HOURSNo-pick auto-cancel window12slot-config.ts:21
TENANT_FEEDBACK_DELAY_HOURSHours after completion → tenant feedback email24feedback-config.ts:32
VENDOR_COMPLETION_REMINDER_DELAY_HOURSHours after visit → vendor proof chase24feedback-config.ts:33
MIN_COMPLETION_PHOTOSMin completion photosunset ⇒ BC setup else 1bc-custom-api.ts:801
AUTO_REPLY_RATE_LIMITMax auto-replies per sender per window40reply-rate-limit.ts:31
AUTO_REPLY_RATE_WINDOW_MINRate-limit window (min)10reply-rate-limit.ts:36
DAILY_BRIEF_MAX_LOG_ROWSCap rows in daily-brief log tableunsetdaily-report.job.ts:378
WEEKLY_BRIEF_MAX_LOG_ROWSSame cap, weeklyunsetweekly-report.job.ts:73

Identity / routing

VarPurposeDefaultRead atEffect when changed
APPROVER_EMAILTHE admin identity (comma-separated) — flags user.isAdmin; escalation + daily/weekly brief recipient; approval recipientunset ⇒ falls back to BC approvalEmail1/2/3admin-mode.preprocessor.ts:42; admin-emails.ts:30Adding 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_IDDefault BC company for admin tools + briefsTRIBECA_COMPANY_IDdaily-report.job.ts:81Re-scopes admin queries and both briefs
LUA_AGENT_IDAgent ID for send-messagenone — throwsemail-notifications.ts:16Identifies the sending agent; unset ⇒ no email
LUA_EMAIL_CHANNEL_IDOutbound email channel UUIDunset ⇒ default chat@heylua.aiemail-notifications.ts:108Set to send from the branded property@… mailbox
BC_ENVIRONMENTBC environment (sandbox vs prod)'Production'bc-client.ts:20Switches BC target. Also gates test-user overrides (forced empty in Production)
AGENT_REPLY_TO_EMAILReply-To on approval emailsproperty@highfieldproperty.ieSendForApprovalTool.ts:256Changes where approvers reply
EMAIL_THREAD_DOMAINDomain for synthetic Message-ID rootsunsetemail-thread-anchor.ts:183Sets the domain in thread anchors
MANAGER_USER_IDOptional in-app daily-report pingnulldaily-report.job.ts:42If set, daily report also sent in-app
VISIT_TIMEZONEIANA zone for visit-date math + brief schedules/labels'Europe/London'agent-timezone.ts:17Changes "today/tomorrow/next Friday" math + brief fire-time. Invalid → falls back to London
AGENTMAIL_INBOX_IDPre-configured AgentMail inboxunset ⇒ auto-createagentmail.ts:61Pins the AgentMail inbox
AGENTMAIL_OVERRIDE_TOForce ALL AgentMail outbound to one addressstefan@heylua.aiagentmail.ts:27Every AgentMail send rewritten here
TEST_USER_PHONE / TEST_USER_EMAILLocal-testing identity overrideunset; forced empty in produser-context.preprocessor.ts:56-57Impersonates a BC customer/vendor for sandbox testing
FINANCE_API_URLOptional finance API base URLunset ⇒ skippedSendForApprovalTool.ts:172With 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

  1. APPROVER_EMAIL is 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.
  2. SLOT_SCHEDULING_ENABLED is "on unless exactly 'false'." A typo or empty value leaves it ON.
  3. APPROVAL_THRESHOLD is overloaded. Setting it >0 also flips auto-approval ON and makes env win over the BC value.
  4. 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:23 fallback (flag for rotation).
  5. Two separate outbound email lanes (LUA_* vs AGENTMAIL_*) — a brief not arriving is a Lua-channel issue, not AgentMail.