← Back

Pipeline is down 20% and the exec meeting is Thursday.

Your dashboard proves it happened. Nobody can tell you why.

Personal project · In development

Project Cortex

A company of AI employees for go-to-market teams. The first hire is Atlas, a data analyst.

The answer exists. It is sitting across GA4, HubSpot, the warehouse and a Slack thread from three weeks ago. Getting it out costs you half a day of the one person on your team who can actually do it, and that person was supposed to be working on next quarter.

So you ration. You ask when the board asks, and the rest of the time you run on instinct and a dashboard that only ever tells you what.

What one answer costs today versus with AtlasTODAYhalf a day of your best analystper questionWITH ATLASminutesper questionThe cost is not the afternoon.It is the questions nobody bothers to ask at that price.

Founders feel this as decisions made on vibes. Heads of growth feel it as a senior hire doing analyst work. Same root cause.

What comes back.

You ask: Why did signups fall last month? WHAT HAPPENED Paid traffic fell 68%, from 9,800 visits to 3,100. [GA4] Conversion did not move: 4.05% to 4.10%. [GA4] So you did not get worse at converting. You stopped getting traffic. WHY The drop starts on one day, 14 June. A Slack message that morning says the campaign budget ran out. [Slack] WHAT I RULED OUT The 14 June deploy ruled out conversion is identical before and after, on mobile and desktop Mobile experience ruled out flat all month Checkout slowdown no data nothing covers that window WHAT TO DO Do not roll back the 14 June deploy. The timing makes it look guilty. It is not. Rolling it back costs a sprint and fixes nothing. Restore campaign budget instead. HOW MUCH TO TRUST THIS Low confidence, 8 caveats. The biggest one: ! your funnel numbers are identical in both periods, which usually means a caching problem, not a real measurement

Shortened from a real run. The test data had a hidden right answer and three convincing wrong ones planted next to it. Atlas found the right one and threw out all three, including the deploy that every timeline would have blamed.

Note what the recommendation saves. The obvious move was a rollback: a sprint of engineering time spent on something that was never the cause, while the real problem, an exhausted campaign budget, kept running.

“Give ChatGPT access to my data and it does this.”

Fair. Connect any good model to your tools, ask why signups fell, and you will get an answer in seconds that reads beautifully.

The problem is not the answer. It is that a right one and a wrong one look identical. Both arrive fluent, confident and well formatted, and you are the one who has to tell them apart, in a meeting, on Thursday.

How an assistant answers versus how Atlas answersANY ASSISTANT WITH ACCESS TO YOUR TOOLSyou askit looks things upan answerchecking it is your jobATLASyou askwhat actually movedwhich part of itbest explanationthen attacks ittry to prove that explanation wrongsurvivesyou get an answer you can act onnothing survivesyou get told so

An assistant stops as soon as it can answer.

That is what it is built to do, and it is why the same question twice can give you two different answers. Atlas is not allowed to stop there. It has to test a competing explanation before it concludes, which is why every report has a section on what it ruled out and why.

An assistant cites. Atlas deletes.

Both show sources. The difference is who does the checking. With an assistant you open each link and verify the numbers yourself, or you skip it, which is what actually happens at 6pm the night before a board deck. With Atlas, a sentence it cannot back never reaches the report.

An assistant has whatever access you handed it.

If it can write to your CRM, it can write to your CRM, and you are trusting it not to. Atlas has no ability to change anything anywhere. That was never built, so there is no permission to get wrong and nothing to explain to your security reviewer.

Honestly: for a quick look, an assistant with tool access is fine and cheaper. The gap shows when the number is going in front of your board, or when the correct call is to do nothing and someone senior needs talking out of a rollback.

How a sentence earns its way into the report.

Two checks every sentence passes before you read itwhat the modelwanted to saydoes every numbercome from a real lookup?does that sourceactually say it?you read itdeleteddeleted, and flagged to youNeither check is the model promising to behave.The first is ordinary code. The second is a second opinion that seesone claim and its source, and nothing else.

The most valuable thing it has done is refuse to answer.

I pointed it at a live CRM and asked why closed-won deals had changed. The numbers said revenue was up about seven times. That is a slide. That is the headline of a board update and possibly the basis of a hiring plan.

Atlas would not report it. Five of the wins carried the exact same close timestamp, which is one import, not five negotiations. Then it checked the open pipeline, which nobody asked it to do, and found deals carrying that same close date while still sitting at “qualified to buy”. A close date on deals that had not closed. As a third check it looked at how many contacts actually became customers. Flat.

Its recommendation was to audit those dates before telling anyone the number. On the way it flagged a duplicate company record, a duplicate contact, and a mistake it had made earlier in its own work.

Put a price on that. A quarter of forecasting off a number that was an import artifact, a board that finds out later, and a CRM nobody trusts again for two quarters. The valuable output was “do not report this yet”, and it took three independent checks to earn it.

Why a workforce and not a tool.

Every tool you buy adds a login and a line item, and someone still has to do the thinking. What teams actually need is the role filled.

So Atlas has a job description rather than a feature list: what it owns, what it may touch, how long it may take, and when it has to stop and come to a human. That description is a config file. Which is the real bet here. The second hire, a product marketing analyst or a sales analyst, is another config file, not another product to buy and integrate.

Cortex as a workforce: Atlas hired, further roles are configurationCORTEXa team you staff, not software you configureATLASdata analysthiredowns explaining why a number movedreads GA4 · HubSpot · warehouse · GitHub · Slackcannot change a single thing, anywhereescalates when the data contradicts itselfproduct marketing analystone more config filesales analystone more config fileA tool ships features. An employee has a job description you can read.

Where it actually is.

In development. I would rather tell you what is missing than imply it is finished.

It is slow. Minutes, not the ninety seconds I set as the target. I can make it faster and it costs accuracy, so I have not.

There is no interface yet. It runs from a terminal. The report view is next.

It has been tested on three scenarios and one real CRM. Enough to show the idea works. Not enough to say it is reliably good.

It does not remember. Every question starts cold, so it does not yet know what a normal week looks like for you. That is the next thing that makes it meaningfully better.

Under the hood.

How the grounding actually works

Every tool call writes one immutable evidence row before anything can be said about it, and the model sees evidence ids rather than free text it could paraphrase. Two independent mechanisms then sit between the draft and the reader. A structural gate drops any claim whose evidence ids do not resolve, in code, before render. An adversarial verifier then re-reads each surviving claim against only the evidence it cites, with nothing else in view, removes the unsupported, downgrades confidence, and discloses the removal as a risk rather than swallowing it. Neither is a prompt instruction, because a prompt is a request and this needed to be a guarantee.

Supporting constraints work the same way. A connector that is not read-only raises at construction, so V1 has no write path by absence of capability. The model never authors SQL against a warehouse: BigQuery exposes reviewed named queries with bound parameters and a byte cap, so a runaway scan fails rather than arriving as an invoice. Every tool schema is closed, so a hallucinated argument surfaces as a correctable error instead of being ignored. Slack text comes back tagged as user-generated content, to cite and never to follow.

Architecture

Four independently deployable services sharing one library: a gateway that is the only externally reachable service and never runs an investigation in the request path, an investigation worker that is the only service talking to a model, an ingest worker for nightly baselines, and a scheduler pinned to one replica because two would double-fire every sync. They communicate through message contracts only, and a test fails the build if one imports another, since a direct sibling import means the two can no longer deploy independently.

Each tenant gets its own graph and its own vector collections, so isolation is a property of which graph you open rather than of a filter someone remembered to add. Graph names are constructed in exactly one place, query language is confined to one module and asserted by an ordinary test, and credentials are envelope encrypted with tenant and provider bound in as authenticated data, so a ciphertext lifted into another tenant's row fails to decrypt rather than leaking.

How it is measured, and what measuring caught

Grounding and accuracy are computed mechanically. Nothing in the scoring path consults a model, because the product's central claim must not be graded by the same kind of component it exists to constrain. Fixtures plant a true causal chain plus decoys that correlate just as strongly, and one scenario is built so the correct output is declining to name a cause at all.

Six measurement defects turned up in a single day, and the pattern is worth stating. The scorer penalised the analyst for explicitly refusing to blame a decoy, because decoy rejection was a substring match and the word appeared in the sentence dismissing it. The hallucination count included claims the gate and verifier had already removed, so reports containing nothing unsupported failed anyway and a thin draft outscored a thorough one. A benchmark harness charged its own broken data to the agent. Each arrived looking like a product weakness. A scorer is code, and untested scoring code is as likely to be wrong as anything else, with the added hazard that its bugs arrive disguised as findings.

External benchmarks are reported the same way: a 40% DABstep result stated with its n=10 confidence interval and an explicit warning not to compare it to the full-set frontier number, and a BIRD run that went 44% to 55% to 65% with the attribution split, ten points from a measurement fix and ten from one prompt sentence. Neither was analyst capability. SWE-bench is not run at all, because an agent with no write tools scores zero and that zero would measure nothing.

Security, audited against my own code

Nineteen findings so far, each with a severity, a fix, and a named regression test. The first was worth the whole exercise: a cross-tenant evidence injection where planted evidence would have made another tenant's data render as a properly grounded citation, with the grounding machinery vouching for it. The second caught an auth stub left fail-open behind a TODO. A later one found the eval suite passing a fabricated conclusion, which is the audit finding you most want to arrive before customers do.

Why I wrote the agent loop instead of adopting a framework

The plan called for building on an existing agent SDK. What I built diverges from that, so I wrote the decision down and then argued against myself in it. Coding agents optimise for a different thing: their tools are bash and file editing, their stop condition is that the task is done, and their benchmark maturity is task-specific and does not transfer to causal analysis. The grounding guarantee depends on controlling exactly what enters context, and if a framework owns the loop that weakens from code-enforced to the framework did not drop the ids, which is not a guarantee.

The part of that divergence that was a mistake is recorded too. Four of the live-path defects I hit were provider transport plumbing that a mature layer would have handled, each found by a separate multi-minute live run. And rather than leave the argument as a position, I built the framework arm anyway and scored it against mine on the same scenarios, which is how the scorer bug surfaced at all.

Built with

PythonFastAPIPostgresFalkorDBQdrantCeleryAnthropic SDKDuckDBDockerpytest