Building Convexa: a compile-time architecture for conversational agents

A product teardown of the system I own at Nagent AI: the architecture, model, cost and go-to-market decisions behind it.

Hariharan V (Hari)  |  Senior Product Manager, Nagent AI  |  linkedin.com/in/vhharan1

See Convexa live: app.convexa.nagent.ai  |  More of my work: haribuilds.com

The problem

Convexa is a conversational execution agent - an AI chat agent, in the term most people use - that a company embeds on its website to talk to its visitors, whether it sells to consumers or to other businesses. It does not just answer questions: it qualifies a lead, writes the result back to the CRM, and hands off scheduling, inside one conversation.

When I took it over it was positioned as a "RAG knowledge chatbot": upload documents, get a bot that answers from them. That category is crowded, it is commoditising fast, and its buyers churn the moment a cheaper option ships. The two competitors we lost deals to both had a specific, repeated complaint in their public reviews: the bot confidently makes things up.

So I made two bets:

  1. Reposition from "answers questions" to "finishes a job." Qualify, sync, hand off - a measurable outcome, not a transcript.
  2. Make not-hallucinating the wedge. Turn the thing customers complain about into the thing we are unambiguously best at.

Everything below follows from those two bets.

The core design: one compiled prompt per customer, assembled at deploy time

A customer never writes a prompt. They answer functional questions in a one-screen onboarding flow ("what should the assistant do? capture leads? resolve issues? both?"). The backend turns those answers into a single system prompt by concatenating fixed blocks:

FINAL SYSTEM PROMPT =
    core guardrails            (identical for every customer, never edited)
  + brand and tone             (merge fields from onboarding: name, voice, languages)
  + admin's custom guardrails  (free text - validated against core before it is accepted)
  + 1-3 intent modules         (from a fixed set of 8: inform, support, qualify,
                                book, account, intake, feedback, promote)
  + 0-N custom intent modules  (compiled on demand when the fixed 8 do not cover a goal)
  + fallback policy            (per active job, a few admin toggles)
  + engagement policy          (idle-visitor re-engagement)
  + an intent router           (added only when 2+ intents are active)

The prompt is recompiled every time an admin changes a setting, and each compiled version is stored, so a deployment is a specific, inspectable artifact rather than a text box someone edited live.

Core guardrails identical for every customer, never edited Brand & tone merge fields from onboarding Admin custom guardrails * free text Intent modules + custom modules * 1-3 of a fixed set of 8 Fallback, engagement & router policies * validated against Core guardrails before it can go live Compile + version at deploy time One system prompt per customer stored · inspectable · new customer live in ~10 minutes
Every customer's system prompt is assembled from fixed blocks at deploy time and stored as a versioned artifact. Nothing is hand-written per customer, and admin free text is checked against Core before it can reach a live prompt.

Why modules instead of a per-customer prompt

The obvious alternative - a prompt-engineer tunes a bespoke prompt per customer - does not scale and does not stay safe. Every hand-written prompt is a place a guardrail can be accidentally dropped, and every change is a regression risk nobody can see.

The tradeoff I accepted: a module system is less expressive than a free hand. A customer with a genuinely unusual job has to either fit one of the eight intents or get a custom module compiled (which is validated the same way). In practice a real deployment uses one to three intents, so the ceiling has not bitten yet. What I bought for that constraint: a new customer goes live in about 10 minutes, and no customer can ship a prompt that violates a core rule.

Runtime: what happens to one visitor message

The compiled prompt is the deploy-time half of the system. This is the per-message half - seven steps between a visitor hitting send and the first word appearing. The prompt tells the model what it should do; the code steps around it make sure that is actually what happens.

Input checks prompt-injection filter · PII redaction Query rewrite message + recent turns become one standalone search query Hybrid retrieval BM25 + semantic, only inside this customer's own index Rerank keep the few chunks that actually answer the question Generate cached compiled prompt + last 5 turns + summary + visitor profile Output grounding check every claim checked against the retrieved chunks Stream reply · call tools first token streamed · CRM push via Composio
One message, end to end. Steps 1 and 6 are code checks, not prompt instructions, so they hold even if a clever message talks the model out of its rules.

Decision: a three-level guardrail precedence, with validation before anything goes live

Rules resolve in a fixed order:

Core Guardrails  >  Admin's Custom Guardrails  >  Intent Module defaults
precedence Core guardrails identity · prompt-injection resistance · grounding · scope safety · escalation · human handoff · never go silent Admin custom guardrails per customer · may restrict Core further, never loosen it Intent module defaults per-intent behaviour when nothing above overrides Release gate golden-dataset eval + adversarial set must pass clean Live per-customer prompt
Rules resolve top down. An admin can tighten Core for their own workspace but cannot loosen it, and no prompt change ships until it clears the eval gate.

Core guardrails are prepended verbatim to every deployment and cover identity and prompt-injection resistance, grounding, scope, safety and escalation, always-available human handoff, and a "never go silent" rule. An admin's own guardrails can restrict further - never loosen. That is not a convention we hope holds; every custom guardrail an admin writes is checked against the core set before it is accepted, and a custom intent module is checked the same way because it is also admin-authored free text heading into a live prompt.

Why this mattered enough to build: the demo that wins B2B deals is "type your own rule and watch it take effect." The demo that loses the renewal is the one where a customer's well-meaning rule quietly disabled the safety layer. Validation-before-live lets us keep the first demo without the second outcome.

Decision: strict grounding as the wedge, with a hard rule against dead ends

The agent answers only from the connected knowledge base, the current conversation, and explicitly connected tools. If the answer is not in one of those, it says so - it does not guess prices, dates, stock levels, or policy. The dashboard can show a "Grounded" tag on answers that are backed by real content, and the phrasing of "I don't have that" is varied so it does not read like a canned line.

The part I care most about: a bare "I don't know" is treated as a failure, not a safe default. The proactivity rule in the core guardrails forces every decline to name a next step in the same breath - an alternative source, a handoff, or a clarifying question. There is one deliberate exception (a "log my issue and leave me alone" intake mode), which is a named, scoped choice rather than a hole in the rule.

The fallback order when the knowledge base comes up empty is explicit: say so plainly, then - only if the business has switched it on - check a short allowlist of trusted outside sites (a retailer's shipping carriers, for example), then hand over a real contact. The one job that skips the outside search is issue resolution: when the agent is trying to fix a customer's actual problem, a guessed fix is worse than no fix, so it escalates to a human straight away.

This is the difference between "safe but useless" and "safe and still moving the conversation forward," and it is why I think of grounding as a growth feature, not a compliance feature.

Decision: an eval suite that gates every prompt release

Because the prompt is assembled from shared modules, a change to one module can regress every customer that uses it. So no prompt change ships without passing a golden-dataset evaluation: a fixed set of representative conversations, each with an expected behaviour (answer from a document, say "I don't know," call the CRM tool, refuse, escalate), scored by a mix of deterministic checks and an LLM-as-judge rubric. Regression against the previous version blocks the release. A separate adversarial set - prompt-injection attempts, jailbreaks, out-of-scope asks - has to pass clean.

Alongside the gate, a Test & Simulate sandbox lets the team and each customer run full multi-turn conversations against a draft assistant before it goes live, without using up their conversation allowance.

This is the least glamorous part of the system and the one that lets me change a shared module without holding my breath.

Decision: onboarding is functional questions, not prompt writing

The version I inherited had an eight-step configuration wizard. Most people did not finish it. I cut it to a single intent-capture screen - the admin picks what the assistant should do, and reaches a clear picture of the assistant's scope in under four clicks. Everything the old wizard asked is still asked, later, in context, with a working assistant already in front of the user to react to. I benchmarked 16 competitor onboarding flows before settling on this; the ones that converted best all deferred configuration until after first value.

Decision: price on the outcome, and only gate the outcome

Pricing is three self-serve tiers plus a custom Enterprise tier, billed per conversation, not per seat - unlimited teammates share one workspace. A $1 trial gives the full entry plan for 14 days.

Two calls I had to defend:

Integration model

CRM sync runs through Composio rather than a hand-rolled integration. When a visitor shows buying intent, the agent captures them as a lead in the background, without stopping the conversation to make them fill in a form, and pushes the lead to the CRM automatically: routing rules, a deal-versus-ticket split, and sentiment and lead-quality scoring written back to the record. If an automatic push fails, a manual "Create Lead" retry appears on that conversation, so a lead is never silently lost. A per-conversation quota feeds metered billing.

Next on the roadmap: image, voice, and video input, voice conversations, calendar booking, live human takeover as a staffed queue, a governed-autonomy (L0-L4) selector, CRMs beyond the first, and channels beyond the website widget.

Data handling

Each customer's knowledge base lives in its own isolated index, so retrieval for one customer can never surface another customer's content. Customer data is not used to train models. Personal data is redacted before it is logged or sent to a model provider (the input check above), which also keeps it out of any vendor that a failover routes to.

Decision: treat latency as a product feature, not an infra afterthought

For a chat-shaped product, the gap between a message and a reply is felt the same way a person going quiet mid-conversation is felt. The metric that matters here is time-to-first-token (TTFT) - how long before the visitor sees anything happen, not how long the full answer takes to finish generating. Early on that gap was 4.7 seconds median - technically correct answers, slow enough to feel broken. Four changes got it to 1.2 seconds, and none of them traded quality for speed:

Decision: match the model to the job, and never depend on one vendor

Convexa makes two very different kinds of LLM call, and treating them as one workload would have meant overpaying on one or underperforming on the other.

So I made two separate calls. Different LLMs for the two prompts - the strongest reasoning model for the scope prompt, where quality compounds, and a small, fast model for chat, where volume does. That split is where the cost saving comes from. And fallbacks for performance - each tier gets two fallback models from different vendors, so a rate limit, outage, or slow response at one provider never degrades or takes down the assistant.

TierPrimary (OpenAI)Fallback 1 (Anthropic)Fallback 2 (Google Vertex AI)
1 - Scope promptFrontier reasoning model
benchmark for instruction following and requirement parsing
Frontier reasoning model
strongest on negative constraints and structured prompt templates
Frontier long-context model
1M-token context for long PRDs and docs without dropping rules
2 - Live chatSmall, fast model
high-volume, low-cost streaming
Small, fast model
warm, natural tone, so failover does not sound robotic
Small, fast model
low-latency tool calling and long chat histories

Cost comes from the split between the two prompts. The chat models cost more than an order of magnitude less per token than the frontier models, and chat is where almost all the tokens are - so the expensive model only runs on the one job that needs it, and the high-volume job runs on the cheap one.

Performance comes from the fallbacks. They are not there to save money; they are there so the assistant keeps performing when the primary cannot. Each one is chosen to hold up on what its tier needs most - reasoning and rule-following for the scope prompt, speed, tone, and tool calling for chat - so a failover keeps the quality bar instead of just keeping the lights on.

trigger: HTTP 429 rate limit · 5xx server error · first token slower than 3.5s Primary OpenAI Fallback 1 Anthropic Fallback 2 Google Vertex AI retry once after 200ms on second failure on secondary failure Conversation state and session carried across every hop exponential backoff + circuit breaker · target: failover in under 300ms
The same chain runs for both tiers. A visitor mid-conversation should never notice which vendor answered, which is why tone and context retention were selection criteria for the chat fallbacks.

Three design rules sit around the chain:

The target is 99.99% availability for the assistant, with no single vendor able to cause an outage. Failover is specified to switch vendors in under 300ms on a 429 or 500 error without losing conversation state.

Unit economics: what one conversation costs

Per-conversation pricing only works if I know what a conversation costs to serve. I modelled it against the entry plan: $29 a month for 50 conversations, or about $0.58 of revenue per conversation.

Assumptions, per conversation: 8 visitor turns. Per turn, about 4,000 tokens of cached compiled prompt, 3,000 uncached tokens (reranked chunks, last 5 turns, summary, the message), and 250 output tokens, plus a small query-rewrite call and the output grounding check. The scope prompt is compiled with a frontier model at about 20,000 tokens in and 5,000 out, and recompiled about 4 times a month as the admin edits settings. Rates per million tokens: chat model $0.20 in, $0.02 cached, $1.25 out; chat fallback $0.80 / $0.08 / $4.00; frontier model $5.00 / $0.50 / $30.00.

Scenario (Starter plan)LLM cost per conversationLLM cost per month% of $29
Tiered, primary models~$0.014~$1.69
$0.69 chat + $1.00 scope prompt
~6%
Tiered, worst case~$0.053~$5.13
every chat on the fallback, 10 recompiles
~18%
Not tiered~$0.34~$18.08
frontier model answering chat too
~62%

Three things this told me:

Results

First paying enterprise customer, a deal I ran end to end from proof-of-concept through security review:

What did not work

What I would do next

  1. Put simulated conversations in the release gate. Automating the multi-turn simulations - a model playing the visitor across whole conversations, run several times per case and scored on whether the agent succeeds every time, not just once - would catch the multi-turn failures a single-exchange eval misses.
  2. Close the loop on unanswered conversations - route every "I don't know" into a weekly knowledge-base gap report, so the number that matters most has an owner and a workflow, not just a dashboard tile.
  3. Ship the autonomy selector. Right now the agent's boldness is fixed per job; customers in regulated spaces want a dial, and an approvals queue behind it.
  4. A second CRM. The Composio layer makes this mostly a config and eval problem rather than an integration rewrite, which was part of why I chose it.

What I owned

Repositioning and the platform's system-prompt architecture; the guardrail precedence and validation model; the eval suite; onboarding; the latency and cost work (knowledge-base migration, context bounding, prompt caching, streaming); the per-message runtime (input checks, query rewrite, hybrid retrieval and rerank, output grounding check); model selection, unit economics, and the multi-vendor failover design; pricing, packaging, and the product-led growth motion; the go-to-market source of truth (competitor matrix, ICP, positioning, launch plan); and the CRM integration spec. Team: cross-functional, engineering and design, at a pre-Series-A company.