# Apex Climate Solutions — Project charter

Brian Parisien · Oct 26, 2026
## 1. Objective and problem

**The build gives Apex Climate Solutions one system that triages incoming requests, answers technicians' field questions with the source cited, and flags operational problems before they cost a job.** Apex Climate Solutions is a simulated company: a regional HVAC (heating, ventilation and air conditioning) business with three service zones, six technicians and six vans.

HVAC was chosen over dental, property management and veterinary because it is the only one of the four where all three components have a real reason to exist. The zones differ on purpose (suburban heat pumps; commercial high-rises with strict permits; rural homes on propane, or LP gas, with long travel times), so that routing, permits and stock each matter.

| Problem today | What the system does about it |
| --- | --- |
| Intake is slow and inconsistent. Requests arrive by phone, web form and WhatsApp, and a dispatcher takes each call and reads each message by hand. | Rates urgency from 1 to 5, matches the customer and equipment to company records, and posts each request to the right Slack channel with a triage window. |
| Field knowledge lives in technicians' heads: refrigerant procedures, city permit rules, warranty terms. Technicians lose time calling someone, or guess. | A Slack lookup that answers from the company's manuals and cites the source document. An answer that cites nothing is blocked. |
| Oversight is reactive. Overdue jobs, empty truck stock and lapsing permits surface only after they cause a missed job or a compliance gap. | A scheduled watchdog posts a digest of overdue jobs, low stock, permits stuck in pending and review drafts waiting for approval. A person approves every public reply. |

Because every message is read by hand, a no-heat emergency can sit behind a routine maintenance request.

### Why this and not off-the-shelf software

This system works on the step before a job is booked: it decides what an ambiguous incoming signal means and what should happen next.

The companion pricing report compares its cost with the three vendors an HVAC shop would consider:

- **Jobber and Housecall Pro** are the mid-tier options.
- **ServiceTitan** publishes no prices; unverified third-party estimates (read Oct 25, 2026) put it at $245 to $500 per technician per month plus $5,000 to $50,000 setup.

What Apex itself offers is graded urgency across channels; cited answers for technicians; permit and stock watching; and a review drafter that writes a reply to each review entered by hand, with a person approving every draft before anything is posted (Yelp replies are posted by hand). Apex is not a full field-service system. At the same call volume, Apex on Jobber's entry tier, keeping Jobber's review add-on for review requests, comes to $307 to $369 a month (estimate), against $332 to $384 at list price for Jobber's own AI phone and review route (read Oct 25–26, 2026). The case for Apex is the features it adds, not price.

## 2. Project scope

**The build delivers a working, reviewable system for one simulated client, not a product.** Each in-scope item can be checked against the built Make.com workflows (called scenarios in Make). No page, video or interview answer may claim an out-of-scope item.

### In scope

- Every message, completed call and form that passes the signature check gets a database record before any judgment is made. Most failures take a safe default or reach a person through Slack or email; the few that don't are named as accepted limits in section 7.
- Urgency is rated 1 to 5 and stays with the request across channels. It has a hard floor: a cheaper model is never allowed to rate a Level 5 (an emergency) lower. Catching every true emergency (Level 5 recall) is the one number that must not slip.
- Technicians get only answers that cite a source. An answer that cites nothing the search returned is blocked.
- Messages to customers use fixed templates (the WhatsApp acknowledgement and the form confirmation email), and a person approves every review reply. The one exception is the hosted phone agent's live conversation, which follows a set script to gather the caller's details.
- Usage is held to a stated ceiling of 10,000 Make operations (Make's billing unit) a month and measured: the first measured month ran 16% over, and fixes are projected, not yet measured, to bring it to 82%.
- The design stands alone and can move between AI model providers. Where it is locked in, the lock-in is named and priced.

### Out of scope

- A real client, real customer data, or a live deployment. All data is visibly synthetic.
- A fourth component, or extra tools added to look busier in place of judgment.
- A link to a job-management system such as Jobber. An overlay design exists, but its blueprint was never imported or tested.
- Any search database other than Postgres with pgvector (an extension that searches text by meaning); any AI agent nested more than one level deep.
- Automatic detection that a source manual changed outside the system.
- A queue that holds failed sends for automatic replay (a dead-letter queue). A send that fails for good raises an alert (the confirmation email also writes an audit row); a person resends it by hand.
- Fixes for the accepted demo limits: a parts report sent twice can deduct stock twice; the review post from the pre-filter cannot retry, because it runs inside a workflow that another workflow calls; the failure-test setup sends a real confirmation email; and the audit login's insert-only limit is enforced by database triggers rather than by the login's permissions.

## 3. Roles and stakeholders

**Five roles use the system, and every action that can't be undone belongs to a person rather than a model.** In this document, names beginning with # are Slack channels.

| Role | Does | Approves or decides | Sees |
| --- | --- | --- | --- |
| Customer | Sends a request by WhatsApp, voice or web form | Nothing in the system; receives templated acknowledgements and the phone agent's scripted conversation | A reply inside the channel they used |
| Dispatcher | Works the triage queue, assigns technicians, sets job status in the admin interface | Scheduling and assignment; whether a Level 5 is acted on; what a cancellation means | #urgent, #normal-triage, the watchdog digest, job threads |
| Technician | Reports parts used and job status by reaction in the job thread; asks the knowledge base | Confirms, discards or undoes their own stock read-back | #tech-help, #parts-used, their own job threads |
| Reviewer (any team member) | Reads each drafted review reply | Whether a reply is published, held or discarded; a Yelp reply is posted by hand | #review-approvals |
| Administrator (project owner) | Loads knowledge documents, holds credentials, owns every store and the cost budget | Every design decision in the log; acceptance of each build; the demo-versus-live table (section 9) | #knowledge-docs, #system-health, all databases |

The system itself holds no approval right. It classifies, drafts, posts and reminds; a person schedules, assigns, publishes and closes.

## 4. The system at a glance

**Every input passes through one of three fixed pipelines and leaves as a Slack post, and all three pipelines share the same data stores in Neon, a hosted Postgres database.**

Requests, documents and scheduled checks enter on the left and leave as Slack posts on the right. Every alert lands in one Slack channel, #system-health, in one fixed format.

## 5. Architecture and stack

**The system runs on Make.com, keeps every data store in Neon, and puts every human touchpoint in Slack.** Make.com is a visual automation platform, used here on its Core plan (10,000 operations a month). From here on, the three components are called C1 (intake and triage), C2 (technician answers from the knowledge base) and C3 (the operations watchdog and review monitor).

The design follows one rule: AI where judgment is needed, fixed rules everywhere else. Each AI call does one job, returns a fixed format, and is checked before anything uses it. Everything around those calls is fixed rules (filters, routers, database functions and scheduled checks), and a person decides every dispatch, stock change and public reply. The hosted phone agent follows a set script to gather a caller's details.

| Layer | What is used | Why |
| --- | --- | --- |
| Automation platform | Make.com Core plan. Each Make workflow (scenario) has one trigger; intake workflows hand off to shared pipelines through Make's built-in workflow-to-workflow call. | Typed hand-offs fix what data each step receives; the free plan's ceiling was measured and exceeded |
| Data | Three separate Neon databases: the document library, searchable by meaning (pgvector); an append-only audit log; and the CRM (customers, jobs, stock, reviews). | One database per store keeps customer data out of the document library by design; Neon scales to zero when idle |
| AI models | Anthropic API (Claude) for intake: junk gate, extraction, urgency rater. Google AI Studio API (Gemini) for document embeddings and the C2 and C3 steps. Retell for voice. | Naming the model for each step makes the tests of cheaper models defensible; a client deployment would run on the client's own API keys |
| Channels in | WhatsApp Business Cloud API (plain webhook); Retell, a hosted AI voice agent (hand-off only after the call completes); Google Form (a script posts each submission) | Make has no form builder (found during the build, September 2026); the free WhatsApp test number cannot use Make's built-in WhatsApp trigger |
| Channels out | Seven Slack channels: #urgent, #normal-triage, #tech-help, #parts-used (one thread per job), #review-approvals, #system-health (one six-line alert format), #knowledge-docs (private uploads) | The only notification channel, chosen over SMS: free per message, one searchable record of alerts and approvals, and where the team already works |
| Reporting | Weekly Google Sheets rollup (built but not scheduled) | Cost |

Make's workflow-to-workflow call passes typed inputs, needs no public web address and uses 0 operations. A webhook is a web address that receives incoming messages. Embeddings are the numeric fingerprints used to search documents by meaning, and each AI step runs on the model that fits its use. Directus, an admin screen generated from the database, sits on the CRM.

### Component 1: intake and triage (C1)

Every request that reaches the pipeline gets a database record before any judgment is made, then runs through fixed steps in order: a duplicate check on the channel's own event ID; a pre-filter (sender present, length in bounds, not blocklisted, fewer than 12 messages per 2 minutes per sender); an exact-match customer lookup; equipment matching by approximate text search inside the database; a voice safety rule (a reported gas or burning smell forces Level 5); a format check on the AI replies; then routing.

Each intake makes at most three AI calls, two for a known customer: a junk gate, an extraction of name and equipment details, and an urgency rater that returns a whole number from 1 to 5 against a written scoring guide. The rater scored 66 of 66 on its development test set.

A Level 5 posts to #urgent, tagged Uncorroborated when nothing beyond the message text backs it; Levels 1 to 4 post to #normal-triage with a triage window. If an AI reply fails the format check or the database fails, no job is created and the raw request goes to #urgent for a person, so that failure is never silent.

### Component 2: technician answers (C2)

Three simulated documents written to look like real vendor material (a diagnostic guide, a permit code covering the three zones, and a warranty policy) are loaded by posting a PDF in a private Slack channel. The loader refuses pages with too little text and documents with a future effective date. It splits each file into 1,800-character passages tagged with their section, converts each file to searchable vectors in one call, and switches to the new version in a single database transaction, so the old version stays live if loading fails.

A technician asks in #tech-help. A fixed nine-phrase rule, not AI, sends stock questions to C3 and everything else to the answer step. The answer comes back as JSON in a fixed structure and must cite passages the search actually returned; the source list is built from database rows, never written by the model, and an answer that cites nothing is blocked. The minimum match score for a passage to count was set at 0.72 on a 16-case test set.

### Component 3: operations watchdog and review monitor (C3)

One database function returns every neglected job with its reason: unassigned for 45 minutes; no technician update for 4 hours; permit pending for 72 hours; truck at or below minimum stock; no job thread after 5 minutes; or assigned with no notice after 10 minutes. It replaced a loop of Make modules (iterator, router and aggregator) that would have cost about 57,000 operations a month.

The watchdog runs every 30 minutes from 7 a.m. to 8 p.m. Panama City time (UTC−5), with reminder-only runs overnight. It calls a model to rank the list only when two or more jobs are flagged, and every flagged job reaches the digest whatever the model returns.

Technicians report parts used in the job thread. An AI step reads the parts back, and the technician confirms, discards or undoes by reaction (✅ confirm, ❌ discard, ↩️ undo). A drafter writes a reply to each review entered by hand, and a person must approve every draft before anything is posted; Yelp replies are posted by hand. Pulling and posting Google and Instagram reviews is designed, not built (section 9). The approval step claims the draft before acting, which prevents double publishing, and nothing is ever published automatically.

### Cross-cutting rules

- Most replies to technicians and approvers retry three times, 15 minutes apart, using Make's built-in retry (called Break); if the retries run out, nobody is told. That retry cannot run in a workflow that another workflow calls, so when the two intake dispatch posts fail they send an email and a #system-health alert instead, which testing confirmed. The missed-call notice, the daily digest of messages set aside as probably not service requests, and the form heartbeat alert (the form script checks in daily) do the same instead of retrying. Alerts are never retried except the form heartbeat watcher's, and writes are never retried except the parts confirm, undo and discard.
- In the knowledge base and the watchdog, AI calls and reads get one quick retry. If that fails and someone is waiting, they get an apology and an alert is raised; if nobody is waiting, only the alert. The three intake AI calls make one attempt, then fall back to a safe default: the message is treated as real, an unmatched field is left unmatched, or an alert is raised.
- The audit log accepts new rows only, enforced by a database trigger; senders are stored hashed and no raw message text is kept. Intake writes a row at each main step; the knowledge base writes one when an answer is blocked, and the watchdog writes rows for review outcomes and stock discards.
- Each workflow runs under its own database login with only the permissions it needs (least privilege). No Make or Directus login can delete or empty a business table; jobs are created through a database function that runs with its owner's rights.
- Operations budget: 84% of plan (estimate), 116% in the first full demo month (measured), and 82% after the fixes (projected). The gap came from unwanted traffic reaching paid steps before a filter stopped it.

### Alternatives considered

| Choice | Chosen | Considered | Why |
| --- | --- | --- | --- |
| Automation platform | Make.com | Zapier; n8n | Make bills per operation and has a visual router, iterator and built-in workflow-to-workflow calls, which suit a fixed-step pipeline |
| Database host | Neon Postgres | Supabase; a separate vector database | The design needed at least three separate databases, and Supabase's free tier capped at two projects (found during the build, September 2026) |
| CRM admin interface | Directus | Baserow; a Make data store | Directus reads an existing database and grants column-level permissions on the free tier; when checked during the build, Baserow could not sit on an existing schema |
| Intake orchestration | Fixed steps, with AI calls where a step needs judgment | A Make AI agent with tools | An AI agent earns its place only with a real tool-calling loop, and the intake cases have none |
| Model providers | Anthropic API for triage; Google AI Studio API for embeddings and knowledge steps | One provider for everything | The only irreversible call, urgency, runs on the strongest available model |
| Voice | Retell (hosted voice agent) | Vapi; a CRM add-on; Meta WhatsApp calling | Retell was chosen for its call-complete webhook and verified payload; the design treats voice as a swappable part |
| Notifications | Slack only | SMS plus Slack | Free per message, one searchable place for alerts, threads and approvals, and where the team already works |

- **Automation platform.** n8n self-hosts cheaply but adds a server to run and secure; its per-execution pricing was noted as the main portability argument and kept as a design note only.
- **Database host.** Three separate databases (document library, audit log, CRM) keep customer data apart from the knowledge base by structure rather than by convention. Neon's free tier allows more projects and suited the demo's volume. Keeping vector search inside Postgres (pgvector) instead of adding a separate vector database also lets all three share one set of access roles. The move from Supabase to Neon changed only the host; the architecture stayed the same.
- **CRM admin interface.** A Make data store holds no relations and no permissions.
- **Intake orchestration.** An AI agent that plans its own steps was considered and set aside. Fixed steps make every audit row a plain write and remove the risk of unbounded cost.
- **Model providers.** Embeddings and answers drawn from the documents held up on Google's free tier, and the documents are simulated, so splitting providers cut cost without changing outcomes.
- **Voice.** Any of the three could fill the slot. WhatsApp calling stayed a design note.
- **Notifications.** Slack reactions and threads work as lightweight approvals. SMS would add a paid vendor, a second channel to monitor and no thread history.

## 6. Design principles

**The hardest part of the build was deciding what not to automate, and six rules guided those decisions.**

1. **Use an AI agent only where a step has a real loop.** An AI agent (a model that plans its own steps) earns its place only when a step must search, get zero, one or many results, and try again. Intake has no such loop: an unknown sender has no records, a known customer with one unit has one row, and a customer with several units needs a person or the customer to decide. The same test on the review monitor found no loop either, so C1 has three single AI calls with fixed reply formats, C3 has four, and the risk of open-ended cost against the operations ceiling cannot occur.
2. **Fixed rules move the data, and a model is called only where the input is ambiguous.** Filters, routers, database functions and fixed phrase rules do the moving. The model answers questions such as: is this junk, what equipment is named, how urgent is it, what does this review say. Every model call returns plain JSON in a fixed structure, and a cleanup step strips any code formatting the model adds.
3. **Choose each model by the cost of a wrong answer.** Only a wrong urgency rating can't be undone, so the rater runs on the strongest model. A cheaper model matched on 43 of 43 cases, then missed six Level 5s on a later prompt version, so the switch was reversed. The junk filter runs on the cheapest model that passed its test set; extraction, which had no formal test, runs on a small, low-cost model.
4. **A customer's identity comes from the channel, never from what the message claims.** Customer lookup is an exact match on the channel's own ID. Approximate matches appear on the triage card as suggestions and are never applied. A public reviewer's name is never matched to a customer record.
5. **Limit what each step can see instead of relying on the prompt.** A field the model never receives cannot leak, so lookup functions return only what the step needs. Telling the model "do not reveal addresses" is a request it can ignore; not giving it the address is a limit it cannot break. The boundaries on what a model can reveal are set by what data a step receives.
6. **Every failure path is spelled out.** In Make, a router's branches do not rejoin and a malformed value matches no branch, so the format check sits before routing. Every send has a chosen retry or alert behavior, and the one unacceptable outcome, a lost request, has its own route to #urgent. The few paths that still end without a post are named as accepted limits in section 7.

Two working rules shaped the record as much as the system. The first is independent scoring: two of the three formal AI tests were first scored in the session that tuned them; all three were re-scored in a separate session before any result was published. The second is that every capability claim about the project is checked against the demo-versus-live table (section 9) before it is used.

## 7. Success criteria and evaluation

**Each success criterion was set as a number before the thing it measures was tested.** The project counts as done well when every criterion below holds. Three rules governed the testing. Each of the three tests had its pass rules written before its first scored run. Two of the three formal AI tests were first scored in the session that tuned them; all three were re-scored in a separate session before any result was published. A pass applies only to the version that was scored, so a prompt or model change triggers a re-run.

| Criterion | Target | Result | How it was tested |
| --- | --- | --- | --- |
| No inbound request is lost | Zero silent drops across all channels | Met for every critical path; every other failure path ends in a Slack post, an email or an audit row, except the limits listed below | Forced failures at each step (bad JSON, database down, timeout) in a throwaway test copy, plus a sweep of every production workflow for silent failures |
| Urgency rating accuracy | Every case within one level of the key; no expected Level 5 rated lower | 66 of 66 on its development set | 66-case development set, one run each; hard fail on any Level 5 miss |
| Every true emergency is still caught after a cheaper model is swapped in (Level 5 recall) | Zero Level 5 misses after any model change | A cheaper model matched on 43 of 43, then missed 6 Level 5s two prompt versions later; the change was reversed | Side-by-side runs of the strongest and the cheaper model on the same answer key |
| Junk gate never discards a real request | Zero real requests marked junk | 0 of 26 real requests marked junk, in 3 runs each, across four models, plus the built filter tested end to end; the chosen model caught 61 of 66 junk runs (92%) at about one-eighth the cost | 48-case answer key run three times per model; the chosen model returned pure JSON in 144 of 144 replies |
| Technician answers cite real sources | Every cited source was actually retrieved; an uncited answer is blocked | Met; match cutoff set at 0.72 with a clear margin (0.68 highest wrong, 0.74 lowest right) | 16-case search test, including look-alike questions the documents don't cover |
| Inbound webhooks reject forged requests | Every unsigned or stale request refused with one alert | Met; one defect found (an empty body crashed the check) and fixed the same day | Forged, replayed and malformed requests against a test copy |
| Operations stay inside the plan | Under 10,000 Make operations a month | Estimated 84%, measured 116%, projected 82% after fixes | One full measured month, then sources of unwanted traffic filtered out before paid steps |
| A person approves every public reply | Nothing publishes without a person's reaction | Met; the approval step claims the draft first, which prevents double publishing | Reaction tests on mock reviews, including a stuck run |

The fixes from the production sweep were tested independently on test copies, except the paths listed below as built but unproven.

**Exceptions to "no inbound request is lost."** Critical paths (a customer request reaches a person, an emergency reaches dispatch) have guards, alerts and backups. Rare, low-impact cases are accepted and recorded. The accepted limits and what each costs:

- A reminder whose count fails to save repeats on the next run: over-alerting, not loss.
- The daily heartbeat check's alert cannot name the missing entries.
- A heartbeat check-in from the wrong source gets "OK" and is caught only by the daily check, up to about 26 hours later.
- If the step that answers the form script fails, the script is not told the outcome.
- An incomplete Google Form response raises an alert that cannot identify the customer. Every question is required, so this happens only if the form or its script is edited.
- One Slack connection carries every alert. Email backs up the dispatch posts, the digest, the heartbeat, the missed-call notice and the message-flood alert, but not the rest.
- Nothing watches the monitors themselves; a real deployment would add an outside uptime check.
- Unused routing for a retired grace period stays in the dispatch step; no job can reach it.
- A technician or approver reply whose retries (3, 15 minutes apart) run out tells nobody, and stock can be deducted while the "saved" reply fails, which leaves the technician no message to undo from.
- Rare silent drops: an uploaded document with no file type, a negative stock quantity and an unknown action in the job-thread step.
- If Slack is down at 8 a.m., that day's digest of messages set aside as probably not service requests is skipped, with an email and an alert, and its items return the next day: one late day.
- A #tech-help photo post is answered from its text, with a note that the photo was not read. That note fires for any shared file, can be missing on odd uploads and is not alerted if it fails: an odd or missing note, rarely.
- If a photo post's hand-off to the answerer fails, the photo note mentions an answer that does not exist: a confusing extra line.
- Some alerts show a double space after "What failed:": looks only.
- Paths closed without an independent test: parts confirm and undo; the document loader's refusal replies, split-failure alert and forced AI retry; the assignment notice with no secret header, two simultaneous edits or a cleared technician. These rare paths are built but unproven.
- Three fixed failure paths were never forced to fail in testing: the low-attention digest failing to post one of its items; the phone-call branch of the intake alert for a failed signature step; a form heartbeat checker alert while Slack is down. They are rare, off the normal path and low impact, so each is built but unproven on that one path.
- If a job's Slack thread is deleted, nothing re-creates it: the watchdog step that would do so was never built.

**Behavior by design:**

- A request that fails the signature check is refused with an alert and answered "OK", so it is not resent: an attacker must not trigger retries.
- If the junk gate fails, the message carries on as real, with the audit row as the trace: a lost real request costs more than junk reaching a person.
- A stock reaction from anyone but the reporting technician changes nothing and gets a refusal reply; only if that reply fails is nothing said.
- New intake alerts fire before the signature check, so while the secrets store is failing, unchecked text can reach #urgent: a noisy alert is preferred to a silent failure.
- Unlisted reactions or outcomes in operations are ignored.
- From a sender's 12th message within 2 minutes, one #urgent alert names the sender; their later messages in that window are dropped without another alert.

## 8. Security, data ownership and platform policy

**The only public entry points are five authenticated Make webhooks.** They receive phone and form intake, WhatsApp intake, the job-assignment notice from Directus, the document loader's uploads, and the Google Form script's daily check-in. Slack events for the other staff channels arrive through Make's own Slack connection, not through a web address this build exposes.

A test copy gets a webhook of its own only while it is under test, and that webhook is removed after verification. No public endpoint is added without a charter entry saying why. A live security check found Neon's Data API (web access to the database) exposed, contradicting an earlier decision; it was deleted without ever being called.

### Security position

- Credentials are never kept in workflow fields, prompts, Jira, the charter or Miro; webhook secrets do appear in Make's private run history, an accepted limit. A credential that appears in any chat or tool result is replaced the same day.
- Inbound webhook checks: a cryptographic signature check (HMAC) proves each Retell, WhatsApp and Slack document-loader request came from the real sender, with a 5-minute age limit on Retell and Slack. The Google Form, its daily check-in and the Directus job-edit hook carry a secret header instead. A refused request posts one alert and gets a success reply (HTTP 200) so the sender does not retry; the accepted cost is that a wrongly refused message must be replayed by hand. If a secret is missing or empty, every request is refused. Forged-request tests verified the checks and found one defect (an empty message body crashed the check), fixed the same day. The document loader's check was added later in the build; it refused forged, unsigned and replayed requests and let a real upload through.
- Least privilege: each workflow has its own database login, none can delete or empty a business table, Directus users see only the columns they are granted, and jobs are created through a function that runs with its owner's rights.
- Personal data: the document library holds no customer data (checked and found empty). Customer details live only in the CRM (mock rows flagged), the audit log and Slack. The audit log never stores raw message text: senders are hashed, emails are redacted, and the only text drawn from a customer's message is a short symptom label.
- Controls adopted from the OWASP Top 10 for large language model applications, a standard security checklist: a dated list of vendors for each step; documents enter only through a private admin channel, with a content fingerprint (hash) and the embedding model recorded for each load; every prompt is treated as public; and no public path leads to the document search store.
- Anything leaving the project (blueprint export, screenshot, board export) is cleaned of webhook addresses, connection IDs, org and project IDs.

### Data ownership and permanence

- The project owner holds every store through accounts the project controls; nothing depends on Make's run history, which it keeps for 30 days. Every store exports as a Postgres dump or blueprint.
- The audit log accepts new rows only, enforced by database triggers; tests as the Make login confirmed it cannot disable or bypass them. The account holder still can, so the log is protected against changed history but is not tamper-proof. A production system would add a copy that cannot be altered for a set period (retention-locked), or a daily export to an account the owner does not administer.
- Customer records are never erased. A customer who leaves is marked declined and kept, so their job history stays intact, and the database itself refuses to delete any customer that has jobs attached. No Make or Directus login has permission to delete in the first place.
- Switching providers: each AI step is a plain web request (HTTP call), then a cleanup step that strips code formatting, then a check against a fixed JSON structure, so a provider swap does not touch later steps. The model name and prompt are set inside each Make module rather than read from a settings table.

### Third-party platform policy

| Platform | What the demo does | What it does not do |
| --- | --- | --- |
| WhatsApp Cloud API | Replies only inside the 24-hour window; a never-expiring system token with a daily check workflow | Never starts a conversation; no model-written text reaches the customer |
| Retell (voice) | Channel adapter on web calls; hand-off after the call completes | No phone number is rented; no triage mid-call |
| Google Business Profile | Mock-review entry through the approval gate | No poll or publish: a simulated company cannot pass Google's API application (found during the build, October 2026) |
| Instagram / Meta | Mock reviews only; checking for new comments could be built later in Meta's development mode | No publish; wider brand monitoring out of scope |
| Yelp | Reads mock reviews; an approved draft is handed to a person | No reply API (partner-only, found during the build, October 2026); the designed poll would break Yelp's storage rule |
| Jobber | Nothing; an untested overlay design would rank and push a lead, never touching the calendar | Not built; the blueprint was never imported or tested |
| Anthropic | Project API key, internal triage only | A client deployment would run on the client's key |
| Gemini free tier | Document embeddings and answers for C2, and the four C3 AI steps: watchdog ranking, parts extraction, part lookup and review drafting; all on simulated data | Real data would need paid quota or a move to Claude |

### Risks and mitigations

| Risk | What it would cost | Mitigation | Residual |
| --- | --- | --- | --- |
| Surge: a heat wave puts 300 to 500 requests a day into one zone with two technicians | The 15-minute Level 5 response becomes arithmetically impossible | Designed, not built: queue with a stated position and an honest estimated arrival time rather than miss the target silently | Accepted; a surge is a staffing problem the software can only make visible |
| Bot flood on the open WhatsApp number | Model calls and operations burn through the plan | Pre-filter before any model call; from a sender's 12th message within 2 minutes, drop after one #urgent alert naming the sender and any open job | Make's own 300-requests-per-10-seconds limit is the only cap on the raw flood |
| Prompt injection in message text | A crafted message steers the classifier | Message text is always untrusted; the model returns an integer against a fixed structure; identity and routing never come from the model | Held in testing; the independent check found a formatting weakness instead, fixed with a cleanup step |
| Free-tier quotas (Google models, Retell credits, Neon) | A step stops mid-day | Quota checked live, not from published tables; model steps moved to the tier with usable limits; one retry then a routed alert | Real customer data would need paid quota |
| Credential expiry | Intake stops without warning | Never-expiring platform token; daily token-check scenario; refusal alerts | Token revocation by the platform would still need a manual regeneration |
| Account holder can rewrite the audit log | Audit trail cannot be called tamper-proof | Append-only by database trigger; the automation login cannot bypass it | Production needs a retention-locked copy outside the owner's control |
| Single region, free tiers, no failover | Outage during a demo | Accepted; the project demonstrates an architecture, not a service-level guarantee | Declined for the demo |
| Operations ceiling overrun | Scenarios stop at the plan limit | Measured monthly; noise filtered before paid steps; watchdog cadence halved | Projection of 82% not yet confirmed by a measured month |

## 9. Demo mode versus a live deployment

**This table is the reference for every capability claim about the project.** Apex demonstrates an architecture and is not a running service: traffic is synthetic, and nothing is deployed for a real company. Built means it exists and ran; Designed means it is drawn and documented but not built; Declined means it is out of scope on purpose.

| Area | Live design | Demo | Status |
| --- | --- | --- | --- |
| Intake on WhatsApp, voice (Retell) and Google Form | Three channels into one pipeline | Same, on a free WhatsApp test number via plain webhook and web-only voice calls | Built |
| Urgency rating, junk gate, extraction | Three single Claude calls with fixed reply formats | Same; rater scored 66 of 66 on its development set | Built |
| Webhook authentication | HMAC and secret-header checks, fail closed | Same; forged-request tested | Built |
| Knowledge base and cited answers | Client manuals loaded by admins | Three simulated documents, 209 chunks, zero customer data | Built |
| Watchdog digest and reminder sweeps | Every 30 minutes, 07:00 to 20:00, reminder-only overnight | Same schedule | Built |
| Parts-used and truck-stock loop | Slack read-back and reaction confirm | Same, on mock stock | Built |
| Review drafting and approval gate | Drafts from Google, Instagram and Yelp; human tick before anything publishes | Mock-review entry through the same gate | Built (gate); polls and publishing below |
| Google and Instagram review poll and publish | Hourly poll; publish after approval | Not connected: a simulated company cannot pass the API applications | Designed |
| Yelp review reply | Handed to a person at any tier | Same | Platform limit |
| Jobber overlay (untested design) | Rank and push a lead into the vendor inbox | Blueprint never imported or tested | Designed |
| Level 5 dispatch | Slack #urgent, tagged when uncorroborated | Same | Built |
| WhatsApp calling | Calls answered by the voice agent via Meta calling | Retell web calls are the only voice channel; no phone number is rented | Designed |
| Meta token check | Daily token-check scenario with alert | Same; a revoked token is regenerated by hand | Built |
| Weekly analytics rollup | Sheets plus Looker Studio | Built but not scheduled | Built |
| Failed-send replay | Queue of failed sends with replay that cannot send twice | Alert; a person resends from the CRM record | Declined |
| Source-change detection for manuals | Detect an edited manual outside the system | Re-post reports Uploaded, Updated or No change | Declined |
| Form-script heartbeat | The form script checks in daily, listing recent entries; a missed check-in or an entry that never arrived raises an alert | Same, built as its own daily check; its scheduled daily run was confirmed | Built |
| Multi-region hosting, automatic failover | n/a | Single region, free tiers | Declined |
| Audit log copy in write-once storage | Retention-locked copy outside the owner's control | Append-only by trigger; owner can still override | Designed |
| Model spend | Paid tiers sized to real volume | Claude credits for C1 only; Gemini free tier elsewhere | Demo constraint |
| Employer access | n/a | Published materials only: case study, this charter, the technical as-built, Cost and eval report pages, the How I build automation page and exported Miro frames. No live webhook on the portfolio site | Deliberate |

## 10. Timeline and status

**The project ran as a phased, largely sequential delivery (waterfall): discovery, then design, then build, then test.** Design decisions kept arriving while build and test were under way, which is why those three bars overlap. The build is complete and its main paths are tested; a few rare paths are built but unproven, listed as accepted limits. The portfolio-site, demo and closeout phase has a target finish of November 15. After the initial launch, add-ons and improvements would move to short iterative cycles (agile, Scrum).

The work was tracked as 139 backlog items, and 110 are done. Closed items include 20 design, 22 build and 14 evaluation and test items; 11 demo and closeout items are in progress. The first charter entry is dated August 20, 2026; the first Make workflow was built in the week of September 14; the first independent test passed on September 27.

## 11. Decision log

**The project owner made every decision in this log; it lists the ones that shaped the architecture, newest first, with the reasoning at the time.** Recording the reasoning lets a later reader tell a decision from an assumption. A superseded entry stays in the log with its replacement named. The full record of about 150 dated decisions is kept in the project's working notes; this table carries the ones a reviewer needs to understand the system.

| Date | Status | Decision | Reasoning at the time | Superseded by |
| --- | --- | --- | --- | --- |
| Oct 26, 2026 | Resolved | Adopt a 1-hour prompt cache on the urgency rater and junk gate | Measured break-even at 51% hit rate; caching does not change answers; a 66-case re-run on Oct 28 matched |  |
| Oct 25, 2026 | Resolved | Watchdog every 30 minutes, 07:00 to 20:00, reminder-only runs overnight | Operations measured at 116% of plan; the 15-minute cadence was the largest avoidable cost |  |
| Oct 25, 2026 | Resolved | Data ownership: audit log protected by triggers only, no write-once copy; raw text never written; model config stays typed per module | The account holder can always override, accepted for a one-person demo; the externalized-config claim was never built, so the charter says so |  |
| Oct 25, 2026 | Resolved | Nine demo limits accepted that day as known gaps, not fixes; the as-built lists eleven from the full pre-demo review | Each was reviewed; none changes the design lesson |  |
| Oct 23, 2026 | Resolved | Junk gate moves from Haiku 4.5 to Haiku 5.5 | 48-case eval: zero real requests marked junk, pure JSON in 144 of 144 runs, about one-eighth the cost | Replaces the Oct 12, 2026 "Haiku 4.5 stays" |
| Oct 23, 2026 | Resolved | Cross-channel job join on sender, customer, phone, email or name-plus-address; a closed job is never rejoined by a message | Makes the persisted-urgency claim true; name alone never joins; a person decides what a cancel means |  |
| Oct 23, 2026 | Resolved | C2 retrieval threshold 0.72 | Highest not-in-corpus score 0.679, lowest answerable 0.738; the answer prompt, not the threshold, guards near-misses |  |
| Oct 19, 2026 | Resolved | Review approval as built: anyone in #review-approvals may approve; Google and Instagram publish stays designed; one database session per call | A named-approver list is production work; a shared session broke both retry and release |  |
| Oct 19, 2026 | Resolved | Dead-letter queue and replay not built | The original plan assumed a queue that does not exist; stored rows are redacted so nothing could be resent from them |  |
| Oct 16, 2026 | Resolved | C2 loader triggered by the project's own Slack app and a custom webhook; PDF only; low-text guard at 100 characters per page | Make's own listener cannot register for private channels; polling is too costly | Replaces the Make Slack listener |
| Oct 13, 2026 | Resolved | WhatsApp system token regenerated as never-expiring; daily check built | A check that cannot run is itself an alert; refresh is needed only if Meta revokes | Replaces the 60-day token (Oct 3, 2026) |
| Oct 9, 2026 | Resolved | WhatsApp reply by session position: full template, then short acknowledgement, then silence, with a per-day ceiling | The first build repeated the same template to every message; 2-hour window chosen over 4 | Refined Oct 26, 2026 (message wording) |
| Oct 7, 2026 | Resolved | Security position: Neon Data API deleted; credentials never kept in workflow fields; secrets-scrubbing rule; four OWASP controls | A live check contradicted the earlier transport decision; cost control already rests on per-step ceilings |  |
| Oct 7, 2026 | Resolved | Inbound webhook authentication: HMAC per sender, custom header for the form, refuse with HTTP 200 plus one alert | Senders retry on non-2xx; Make's API-key option blocks the request before any alert can fire |  |
| Oct 3, 2026 | Resolved | Expiry state lives in Neon, swept inside the watchdog run; fixed Break delays instead of reading Retry-After | Durable state in one place; vendor docs on Retry-After are inconsistent | Replaces the Make Data Store |
| Oct 2, 2026 | Resolved | No agent in the C3 review monitor | The loop test found no loop; a person reads every draft, so autonomy buys nothing the gate does not |  |
| Oct 1, 2026 | Resolved | One six-line alert format for #system-health; one quick retry on AI calls and reads, routed by whether someone is waiting | Scannable without opening Make; daily caps do not clear within a run |  |
| Oct 1, 2026 | Resolved | Job assignment loop: Directus Flow notifies Make; technician reacts 🚗 or 🏁 in the job thread | Technician learns in about 25 minutes instead of 4 hours |  |
| Sep 28, 2026 | Resolved | Urgency rater on Opus 5.5, reversing the Sonnet 5 downsize | Sonnet 5 missed six Level 5s on prompt v4; the Level 5 floor outweighs cost | Replaces Sep 27, 2026 downsize |
| Sep 27, 2026 | Resolved | Every Level 5 posts to #urgent; uncorroborated ones tagged | A false 5 now costs a tagged post, not a dispatch | Replaces uncorroborated-to-#normal-triage |
| Sep 25, 2026 | Resolved | Schema-invalid AI output takes the intake-failure route; outbound sends retry 3 times 15 minutes apart; review approval claims the draft before acting | A lost intake is the one unacceptable outcome; Slack outages heal; prevents double publish |  |
| Sep 23, 2026 | Resolved | Watchdog breach check runs as one database function; C3 steps are stored procedures; all stores on Neon; seven Slack channels | The iterator-router design would have cost about 57,000 operations a month | Replaces the Sep 22, 2026 iterator design and the four-channel list |
| Sep 22, 2026 | Resolved | No AI agent orchestrating C1 intake: deterministic control flow with three single AI calls; resolution is a database function | No real tool-calling loop exists in the intake cases | Replaces the Sep 10, 2026 one-agent topology |
| Sep 22, 2026 | Resolved | Google Form enters by Apps Script post, not polling; confirmation email sent only after the record exists | Polling would cost about 43,200 operations a month; confirming before insert misleads the customer |  |
| Sep 20, 2026 | Resolved | CRM in its own Neon project; one least-privilege role per scenario, none with DELETE; Directus shares the database | The project is the isolation boundary, so "zero customer data in the corpus" becomes structural |  |
| Sep 20, 2026 | Resolved | OCR skipped; a low-text guard refuses image-only PDFs | The corpus is text-based; the guard needs no model call | Replaces the raw-HTTP OCR transport decided earlier the same day |
| Sep 19, 2026 | Resolved | Three intake channels (WhatsApp, Retell voice, Google Form); escalation is Slack-only; SMS retired; every intake creates a record immediately | Make has no form builder; Slack carries the demo path; human confirmation applies to scheduling, not record creation | Replaces "WhatsApp is the exclusive front door" and the SMS on-call roster |
| Sep 19, 2026 | Resolved | Scope the tool, not the prompt: lookups return only name, status, make/model and service dates | A field absent from context cannot leak |  |
| Sep 12, 2026 | Resolved | Open-item tracking moves to Jira; Directus is the admin layer on the Neon schema | Visibility and status; Baserow could not introspect an existing database |  |
| Sep 12, 2026 | Resolved | Urgency engine is advisory only and never edits a booking or calendar | No scheduling system bumps slots; emergencies bypass the calendar |  |
| Sep 11, 2026 | Resolved | Knowledge-base answers (C2) and watchdog ranking, parts and review steps (C3) on Gemini Flash Lite; embedding model pinned | Full Flash models capped at 20 requests a day on the free tier; changing the embedding model means re-embedding the corpus |  |
| Sep 10, 2026 | Resolved | Idempotency by a database unique constraint on the channel event ID; validation before the router; Instagram DM removed from intake | Make runs webhooks in parallel so read-then-check races; a malformed value matches no route and vanishes |  |
| Sep 8, 2026 | Resolved | Model tier follows failure cost; customer lookup is deterministic exact match; operating modes with explicit degradation (designed, not built) | Only urgency has irreversible cost; the 15-minute Level 5 response target is arithmetically impossible in a surge, so queue with an honest estimated arrival time |  |
| Sep 8, 2026 | Resolved | Database host moves from Supabase to Neon; Make upgraded to Core; portability boundary accepted as named, priced lock-in | Supabase free tier caps at two projects; free Make limits were measured; native orchestration justified on reliability, not cost |  |
| Aug 31, 2026 | Resolved | HVAC chosen as the domain | The only domain where all three components have a real reason to exist |  |

## 12. Companion documents

**The charter holds intent and decisions; three other records hold everything else.** Each kind of information has one home, so the records do not drift apart.

| Layer | Holds | Where |
| --- | --- | --- |
| Charter (this document) | Objective, scope, architecture, principles, decisions and why | Portfolio site |
| Jira (work tracker) | What is open: build, verification and test items, each with what must be true for it to count as finished | Private; shown in interviews |
| As-built and test record | How the system works today: build results, checks of the data passed between steps, test runs, module versions, evaluation tables | Portfolio site |
| Architecture board | A drawn view of the charter, redrawn after the charter changes | Miro |

Supporting documents: the pricing report (a cost comparison of Jobber, Housecall Pro, ServiceTitan and Apex's own running cost of about $59 to $121 a month, an estimate).
