Skip to content
Menu
Apex Climate Solutions

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 todayWhat 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.

RoleDoesApproves or decidesSees
CustomerSends a request by WhatsApp, voice or web formNothing in the system; receives templated acknowledgements and the phone agent's scripted conversationA reply inside the channel they used
DispatcherWorks the triage queue, assigns technicians, sets job status in the admin interfaceScheduling and assignment; whether a Level 5 is acted on; what a cancellation means#urgent, #normal-triage, the watchdog digest, job threads
TechnicianReports parts used and job status by reaction in the job thread; asks the knowledge baseConfirms, discards or undoes their own stock read-back#tech-help, #parts-used, their own job threads
Reviewer (any team member)Reads each drafted review replyWhether 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 budgetEvery 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.

Deterministic pipelines with bounded AI calls, all on one record store

Inbound requestsWhatsApp, Retell voice,Google FormC1 Intake and triageRecord first, then judgededup, pre-filter, exact customer lookupthree AI calls, fixed rulesTriage posts#urgent (Level 5)#normal-triage (1 to 4)Admin posts a PDFin #knowledge-docsTechnician asksin #tech-helpC2 Knowledge baseloader: chunk, embed, activate atomicallyanswerer: cited JSON, uncited is blockedfixed phrase rule sorts stock questionsCited answerreply in the threadScheduleevery 30 minutes07:00 to 20:00Reviewsmock review entryC3 Watchdog and reviewsbreach check runs in the databasedigest, expiry sweeps, parts read-backjob assignment and technician loopreview drafter, then human approvalDigest and approvals#normal-triage digest#review-approvals gate#system-health alertsNeon Postgres, shared by all three componentscorpus with pgvector | audit log, append-only by trigger | CRM with Directus admin, one least-privilege role per scenario
  • Fixed rule
  • Slack or input
  • Store

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.

LayerWhat is usedWhy
Automation platformMake.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
DataThree 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 modelsAnthropic 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 inWhatsApp 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 outSeven 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
ReportingWeekly 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

ChoiceChosenConsideredWhy
Automation platformMake.comZapier; n8nMake bills per operation and has a visual router, iterator and built-in workflow-to-workflow calls, which suit a fixed-step pipeline
Database hostNeon PostgresSupabase; a separate vector databaseThe design needed at least three separate databases, and Supabase's free tier capped at two projects (found during the build, September 2026)
CRM admin interfaceDirectusBaserow; a Make data storeDirectus 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 orchestrationFixed steps, with AI calls where a step needs judgmentA Make AI agent with toolsAn AI agent earns its place only with a real tool-calling loop, and the intake cases have none
Model providersAnthropic API for triage; Google AI Studio API for embeddings and knowledge stepsOne provider for everythingThe only irreversible call, urgency, runs on the strongest available model
VoiceRetell (hosted voice agent)Vapi; a CRM add-on; Meta WhatsApp callingRetell was chosen for its call-complete webhook and verified payload; the design treats voice as a swappable part
NotificationsSlack onlySMS plus SlackFree 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.

CriterionTargetResultHow it was tested
No inbound request is lostZero silent drops across all channelsMet for every critical path; every other failure path ends in a Slack post, an email or an audit row, except the limits listed belowForced 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 accuracyEvery case within one level of the key; no expected Level 5 rated lower66 of 66 on its development set66-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 changeA cheaper model matched on 43 of 43, then missed 6 Level 5s two prompt versions later; the change was reversedSide-by-side runs of the strongest and the cheaper model on the same answer key
Junk gate never discards a real requestZero real requests marked junk0 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 cost48-case answer key run three times per model; the chosen model returned pure JSON in 144 of 144 replies
Technician answers cite real sourcesEvery cited source was actually retrieved; an uncited answer is blockedMet; 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 requestsEvery unsigned or stale request refused with one alertMet; one defect found (an empty body crashed the check) and fixed the same dayForged, replayed and malformed requests against a test copy
Operations stay inside the planUnder 10,000 Make operations a monthEstimated 84%, measured 116%, projected 82% after fixesOne full measured month, then sources of unwanted traffic filtered out before paid steps
A person approves every public replyNothing publishes without a person's reactionMet; the approval step claims the draft first, which prevents double publishingReaction 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

PlatformWhat the demo doesWhat it does not do
WhatsApp Cloud APIReplies only inside the 24-hour window; a never-expiring system token with a daily check workflowNever starts a conversation; no model-written text reaches the customer
Retell (voice)Channel adapter on web calls; hand-off after the call completesNo phone number is rented; no triage mid-call
Google Business ProfileMock-review entry through the approval gateNo poll or publish: a simulated company cannot pass Google's API application (found during the build, October 2026)
Instagram / MetaMock reviews only; checking for new comments could be built later in Meta's development modeNo publish; wider brand monitoring out of scope
YelpReads mock reviews; an approved draft is handed to a personNo reply API (partner-only, found during the build, October 2026); the designed poll would break Yelp's storage rule
JobberNothing; an untested overlay design would rank and push a lead, never touching the calendarNot built; the blueprint was never imported or tested
AnthropicProject API key, internal triage onlyA client deployment would run on the client's key
Gemini free tierDocument embeddings and answers for C2, and the four C3 AI steps: watchdog ranking, parts extraction, part lookup and review drafting; all on simulated dataReal data would need paid quota or a move to Claude

Risks and mitigations

RiskWhat it would costMitigationResidual
Surge: a heat wave puts 300 to 500 requests a day into one zone with two techniciansThe 15-minute Level 5 response becomes arithmetically impossibleDesigned, not built: queue with a stated position and an honest estimated arrival time rather than miss the target silentlyAccepted; a surge is a staffing problem the software can only make visible
Bot flood on the open WhatsApp numberModel calls and operations burn through the planPre-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 jobMake's own 300-requests-per-10-seconds limit is the only cap on the raw flood
Prompt injection in message textA crafted message steers the classifierMessage text is always untrusted; the model returns an integer against a fixed structure; identity and routing never come from the modelHeld 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-dayQuota checked live, not from published tables; model steps moved to the tier with usable limits; one retry then a routed alertReal customer data would need paid quota
Credential expiryIntake stops without warningNever-expiring platform token; daily token-check scenario; refusal alertsToken revocation by the platform would still need a manual regeneration
Account holder can rewrite the audit logAudit trail cannot be called tamper-proofAppend-only by database trigger; the automation login cannot bypass itProduction needs a retention-locked copy outside the owner's control
Single region, free tiers, no failoverOutage during a demoAccepted; the project demonstrates an architecture, not a service-level guaranteeDeclined for the demo
Operations ceiling overrunScenarios stop at the plan limitMeasured monthly; noise filtered before paid steps; watchdog cadence halvedProjection 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.

AreaLive designDemoStatus
Intake on WhatsApp, voice (Retell) and Google FormThree channels into one pipelineSame, on a free WhatsApp test number via plain webhook and web-only voice callsBuilt
Urgency rating, junk gate, extractionThree single Claude calls with fixed reply formatsSame; rater scored 66 of 66 on its development setBuilt
Webhook authenticationHMAC and secret-header checks, fail closedSame; forged-request testedBuilt
Knowledge base and cited answersClient manuals loaded by adminsThree simulated documents, 209 chunks, zero customer dataBuilt
Watchdog digest and reminder sweepsEvery 30 minutes, 07:00 to 20:00, reminder-only overnightSame scheduleBuilt
Parts-used and truck-stock loopSlack read-back and reaction confirmSame, on mock stockBuilt
Review drafting and approval gateDrafts from Google, Instagram and Yelp; human tick before anything publishesMock-review entry through the same gateBuilt (gate); polls and publishing below
Google and Instagram review poll and publishHourly poll; publish after approvalNot connected: a simulated company cannot pass the API applicationsDesigned
Yelp review replyHanded to a person at any tierSamePlatform limit
Jobber overlay (untested design)Rank and push a lead into the vendor inboxBlueprint never imported or testedDesigned
Level 5 dispatchSlack #urgent, tagged when uncorroboratedSameBuilt
WhatsApp callingCalls answered by the voice agent via Meta callingRetell web calls are the only voice channel; no phone number is rentedDesigned
Meta token checkDaily token-check scenario with alertSame; a revoked token is regenerated by handBuilt
Weekly analytics rollupSheets plus Looker StudioBuilt but not scheduledBuilt
Failed-send replayQueue of failed sends with replay that cannot send twiceAlert; a person resends from the CRM recordDeclined
Source-change detection for manualsDetect an edited manual outside the systemRe-post reports Uploaded, Updated or No changeDeclined
Form-script heartbeatThe form script checks in daily, listing recent entries; a missed check-in or an entry that never arrived raises an alertSame, built as its own daily check; its scheduled daily run was confirmedBuilt
Multi-region hosting, automatic failovern/aSingle region, free tiersDeclined
Audit log copy in write-once storageRetention-locked copy outside the owner's controlAppend-only by trigger; owner can still overrideDesigned
Model spendPaid tiers sized to real volumeClaude credits for C1 only; Gemini free tier elsewhereDemo constraint
Employer accessn/aPublished 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 siteDeliberate

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).

Design, build and test ran in parallel; site, demo and closeout are under way

Phases by date, 2026. Bars show first and last recorded activity per phase; the current phase runs to the target.

Aug 17Aug 31Sep 14Sep 28Oct 12Oct 26Nov 9Aug 20 to Sep 13Discovery and planningSep 8 to Oct 25Design and decisionsSep 14 to Oct 26BuildSep 27 to Oct 26Test and evaluationOct 27 to Nov 15Site, demo and closeoutNov 15Wrap-up target
  • First to last recorded activity
  • Wrap-up target

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.

DateStatusDecisionReasoning at the timeSuperseded by
Oct 26, 2026ResolvedAdopt a 1-hour prompt cache on the urgency rater and junk gateMeasured break-even at 51% hit rate; caching does not change answers; a 66-case re-run on Oct 28 matched
Oct 25, 2026ResolvedWatchdog every 30 minutes, 07:00 to 20:00, reminder-only runs overnightOperations measured at 116% of plan; the 15-minute cadence was the largest avoidable cost
Oct 25, 2026ResolvedData ownership: audit log protected by triggers only, no write-once copy; raw text never written; model config stays typed per moduleThe 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, 2026ResolvedNine demo limits accepted that day as known gaps, not fixes; the as-built lists eleven from the full pre-demo reviewEach was reviewed; none changes the design lesson
Oct 23, 2026ResolvedJunk gate moves from Haiku 4.5 to Haiku 5.548-case eval: zero real requests marked junk, pure JSON in 144 of 144 runs, about one-eighth the costReplaces the Oct 12, 2026 "Haiku 4.5 stays"
Oct 23, 2026ResolvedCross-channel job join on sender, customer, phone, email or name-plus-address; a closed job is never rejoined by a messageMakes the persisted-urgency claim true; name alone never joins; a person decides what a cancel means
Oct 23, 2026ResolvedC2 retrieval threshold 0.72Highest not-in-corpus score 0.679, lowest answerable 0.738; the answer prompt, not the threshold, guards near-misses
Oct 19, 2026ResolvedReview approval as built: anyone in #review-approvals may approve; Google and Instagram publish stays designed; one database session per callA named-approver list is production work; a shared session broke both retry and release
Oct 19, 2026ResolvedDead-letter queue and replay not builtThe original plan assumed a queue that does not exist; stored rows are redacted so nothing could be resent from them
Oct 16, 2026ResolvedC2 loader triggered by the project's own Slack app and a custom webhook; PDF only; low-text guard at 100 characters per pageMake's own listener cannot register for private channels; polling is too costlyReplaces the Make Slack listener
Oct 13, 2026ResolvedWhatsApp system token regenerated as never-expiring; daily check builtA check that cannot run is itself an alert; refresh is needed only if Meta revokesReplaces the 60-day token (Oct 3, 2026)
Oct 9, 2026ResolvedWhatsApp reply by session position: full template, then short acknowledgement, then silence, with a per-day ceilingThe first build repeated the same template to every message; 2-hour window chosen over 4Refined Oct 26, 2026 (message wording)
Oct 7, 2026ResolvedSecurity position: Neon Data API deleted; credentials never kept in workflow fields; secrets-scrubbing rule; four OWASP controlsA live check contradicted the earlier transport decision; cost control already rests on per-step ceilings
Oct 7, 2026ResolvedInbound webhook authentication: HMAC per sender, custom header for the form, refuse with HTTP 200 plus one alertSenders retry on non-2xx; Make's API-key option blocks the request before any alert can fire
Oct 3, 2026ResolvedExpiry state lives in Neon, swept inside the watchdog run; fixed Break delays instead of reading Retry-AfterDurable state in one place; vendor docs on Retry-After are inconsistentReplaces the Make Data Store
Oct 2, 2026ResolvedNo agent in the C3 review monitorThe loop test found no loop; a person reads every draft, so autonomy buys nothing the gate does not
Oct 1, 2026ResolvedOne six-line alert format for #system-health; one quick retry on AI calls and reads, routed by whether someone is waitingScannable without opening Make; daily caps do not clear within a run
Oct 1, 2026ResolvedJob assignment loop: Directus Flow notifies Make; technician reacts 🚗 or 🏁 in the job threadTechnician learns in about 25 minutes instead of 4 hours
Sep 28, 2026ResolvedUrgency rater on Opus 5.5, reversing the Sonnet 5 downsizeSonnet 5 missed six Level 5s on prompt v4; the Level 5 floor outweighs costReplaces Sep 27, 2026 downsize
Sep 27, 2026ResolvedEvery Level 5 posts to #urgent; uncorroborated ones taggedA false 5 now costs a tagged post, not a dispatchReplaces uncorroborated-to-#normal-triage
Sep 25, 2026ResolvedSchema-invalid AI output takes the intake-failure route; outbound sends retry 3 times 15 minutes apart; review approval claims the draft before actingA lost intake is the one unacceptable outcome; Slack outages heal; prevents double publish
Sep 23, 2026ResolvedWatchdog breach check runs as one database function; C3 steps are stored procedures; all stores on Neon; seven Slack channelsThe iterator-router design would have cost about 57,000 operations a monthReplaces the Sep 22, 2026 iterator design and the four-channel list
Sep 22, 2026ResolvedNo AI agent orchestrating C1 intake: deterministic control flow with three single AI calls; resolution is a database functionNo real tool-calling loop exists in the intake casesReplaces the Sep 10, 2026 one-agent topology
Sep 22, 2026ResolvedGoogle Form enters by Apps Script post, not polling; confirmation email sent only after the record existsPolling would cost about 43,200 operations a month; confirming before insert misleads the customer
Sep 20, 2026ResolvedCRM in its own Neon project; one least-privilege role per scenario, none with DELETE; Directus shares the databaseThe project is the isolation boundary, so "zero customer data in the corpus" becomes structural
Sep 20, 2026ResolvedOCR skipped; a low-text guard refuses image-only PDFsThe corpus is text-based; the guard needs no model callReplaces the raw-HTTP OCR transport decided earlier the same day
Sep 19, 2026ResolvedThree intake channels (WhatsApp, Retell voice, Google Form); escalation is Slack-only; SMS retired; every intake creates a record immediatelyMake has no form builder; Slack carries the demo path; human confirmation applies to scheduling, not record creationReplaces "WhatsApp is the exclusive front door" and the SMS on-call roster
Sep 19, 2026ResolvedScope the tool, not the prompt: lookups return only name, status, make/model and service datesA field absent from context cannot leak
Sep 12, 2026ResolvedOpen-item tracking moves to Jira; Directus is the admin layer on the Neon schemaVisibility and status; Baserow could not introspect an existing database
Sep 12, 2026ResolvedUrgency engine is advisory only and never edits a booking or calendarNo scheduling system bumps slots; emergencies bypass the calendar
Sep 11, 2026ResolvedKnowledge-base answers (C2) and watchdog ranking, parts and review steps (C3) on Gemini Flash Lite; embedding model pinnedFull Flash models capped at 20 requests a day on the free tier; changing the embedding model means re-embedding the corpus
Sep 10, 2026ResolvedIdempotency by a database unique constraint on the channel event ID; validation before the router; Instagram DM removed from intakeMake runs webhooks in parallel so read-then-check races; a malformed value matches no route and vanishes
Sep 8, 2026ResolvedModel 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, 2026ResolvedDatabase host moves from Supabase to Neon; Make upgraded to Core; portability boundary accepted as named, priced lock-inSupabase free tier caps at two projects; free Make limits were measured; native orchestration justified on reliability, not cost
Aug 31, 2026ResolvedHVAC chosen as the domainThe 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.

LayerHoldsWhere
Charter (this document)Objective, scope, architecture, principles, decisions and whyPortfolio site
Jira (work tracker)What is open: build, verification and test items, each with what must be true for it to count as finishedPrivate; shown in interviews
As-built and test recordHow the system works today: build results, checks of the data passed between steps, test runs, module versions, evaluation tablesPortfolio site
Architecture boardA drawn view of the charter, redrawn after the charter changesMiro

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).