For fifty years, putting data into a system has started the same way: stop, design a schema, and commit to it before the first row lands. Create, Read, Update, Delete — four verbs, all of which assume you already know what your data looks like and how you'll ask for it later.
That assumption is the expensive part. Not the databases, not the ORMs, not the migrations — the requirement that you do the structural thinking up front, and keep doing it every time the data changes shape.
Miriel's API has two verbs instead: learn() and query(). This post is about why that isn't a simplification of CRUD, but a replacement for it — the structure moves from something you design to something the system evolves, and the access patterns change completely as a result.
The tax CRUD charges before the first byte#
Consider what it takes to store a customer contract in a CRUD world. You design a contracts table. You decide which fields matter — customer_id, renewal_date, amount — which means deciding, in advance, every question you'll ever ask. You write a parser to get the PDF into those columns. Then a support ticket arrives that contradicts the contract, and it lives in a different table, with a different schema, written by a different team, and no query joins them because nobody designed for that question.
Every CRUD system embeds a prediction: here is what we will want to know. When the prediction is wrong — and it always eventually is — you pay again: migrations, backfills, new parsers, new join tables. The schema isn't a description of your data. It's a bet about your future questions, made at the moment you know the least.
Verb one: learn()#
Here is the entire ingestion story for the contract, the ticket archive, and a note from the account manager:
requests.post("https://api.miriel.ai/api/v2/learn", json={
"input": [
{"value": "https://acme.example.com/msa-2026-renewal.pdf"},
{"value": "s3://support-exports/acme-tickets/"},
{"value": "Acme renewed at $180k/yr. Contract runs through June 2027, "
"with a 60-day out clause tied to uptime SLAs."},
],
"project": ["acme"],
})There is no schema in this call. No content-type declarations, no field mappings, no "first create a collection." A URL, a bucket, and a plain sentence go into the same list. The first thing behind learn() is a router: it inspects each item, figures out what it is — a document to parse, an image to embed, a bucket to walk, a live stream to subscribe to, a table of events to keep as rows — and dispatches it to the pipeline that fits. Then each pipeline does the structural work you used to do at design time:
- Text is chunked and embedded into a semantic index; images get their own embedding space. The "columns" here are 768 dimensions nobody had to name.
- Metadata is extracted, not declared. Source, provider, timestamps, deep-link paths back to the original — captured per item as it flows through, so provenance exists without a provenance table.
- Entities and relationships are distilled by a background worker into knowledge-graph triples: Acme — renewed_at — $180k/yr, MSA — expires — June 2027. The graph is the schema the data implied, discovered after the fact.
- Structured data keeps its structure. Events keep their timestamps and land as time series; tabular data keeps its rows and columns. Schemaless ingestion doesn't mean flattening everything into text — it means the router recognizes the shape the data already has and preserves it, instead of asking you to declare it.
The one structural decision left to you is project — a label, closer to a folder than a schema. Everything else that CRUD makes you decide up front, learn() derives.
The schema you didn't write keeps rewriting itself#
The deeper break with CRUD isn't that the structure is inferred once. It's that it's alive.
In a relational system, the schema is the most frozen thing you own. In Miriel, the derived structure evolves continuously as data arrives. When a second document corroborates a relationship the graph already holds, that edge strengthens. When nothing new supports it, it decays — relationship strength has a 30-day half-life, so the graph's shape drifts toward what your data currently says rather than what it said last quarter. New entities appear the moment a document first mentions them; no ALTER TABLE, no migration PR, no backfill ticket.
This inverts the CRUD lifecycle. There, structure is decided before data and changed reluctantly afterward. Here, data comes first and structure is a running summary of it — recomputed, reinforced, and forgotten at the same cadence the data itself changes. You could not maintain this schema by hand, because it changes every time you learn something.
Verb two: query() — and why the access pattern is the real story#
CRUD reads are structural addressing: to get anything out, you specify where it lives — table, keys, join path. SELECT amount FROM contracts WHERE customer_id = 42 works precisely because someone predicted that question at design time.
query() is semantic addressing. You ask the question; the system decides which representations answer it:
requests.post("https://api.miriel.ai/api/v2/query", json={
"query": "What did we commit to Acme in the renewal, and has anything "
"in support tickets since then contradicted it?",
"project": ["acme"],
})One call. Behind it, the first thing that happens isn't retrieval — it's a decision. Just as learn() has a router on the way in, query() has one on the way out. It reads the question and classifies what kind of answer it demands: a targeted search that a handful of relevant documents can satisfy, an exhaustive pass that must aggregate across the whole corpus ("what's the sum of every statement balance?"), or a temporal reconstruction that must order events and rebuild state as of a moment in time ("what did the account look like last Tuesday?"). CRUD makes you encode that decision — the JOIN, the GROUP BY, the WHERE on timestamps. Here the query plan is derived from the question itself.
Then the fan-out, across every structure learn() built. The vector indexes surface semantically relevant chunks from the contract PDF and the ticket archive. The knowledge graph is traversed for what's connected to Acme — including facts two hops away that vector search alone would miss. Relational data answers with filters and aggregates over its rows; time-series data answers with windows and trends over its timestamps. A model composes the answer grounded in the full retrieved set, with every source document attached for verification.
But the real power isn't any single structure — it's the combination. Ask: "Did our ticket response times get worse after the renewal was signed, and did that break what we committed to in the SLA?" That one sentence needs four different kinds of data: semantic retrieval to find the SLA language in the contract, the graph to connect the SLA to Acme's renewal, a relational aggregate over ticket rows to compute response times, and a time series to split them before and after the signing date. In a CRUD world, that's not a query — it's an integration project spanning a document store, a ticketing database, and a BI tool, glued together by hand. Here it's a sentence, because the router decomposes it and every data shape it needs already lives on one substrate.
Notice what you did not specify: which tables to hit, how to join a PDF to a ticket export, which fields were relevant, or even which modality the answer lives in. The response tells you which documents it used — including their type, because a text query can legitimately be answered by an image, a table, or a timeline.
This is the part CRUD never had an answer for. Its four verbs were honest about writes — Create, Update, Delete map cleanly onto storage mutations — but "Read" was always a placeholder for the hard problem. Two routers absorb that problem: one classifies data on the way in so nothing needs a schema, one classifies questions on the way out so nothing needs a query plan. That's why two verbs are enough.
The verbs also compose in a way CRUD's never did: query results can themselves be learned, so answers become context for future questions. Reads that feed writes are an anti-pattern in CRUD. Here they're how the system gets smarter.
What happened to Update and Delete?#
They didn't disappear — they stopped being verbs you conjugate by hand and became properties of learn().
Pass an upsert_id and the new content replaces (or appends to) what that ID learned before — versioned re-learning rather than row surgery. Pass expiration_seconds and the document removes itself on schedule — retention as a declaration at write time, not a cron job you write later. In both cases you state the lifecycle of the knowledge, and the system handles the mutation mechanics: re-chunking, re-embedding, re-extracting entities, letting stale graph edges decay. Update and Delete were always about keeping stored structure consistent with reality; when the structure is derived, consistency is the pipeline's job, not yours.
One substrate, one query: Percept#
All of this rests on a question CRUD never had to answer: what kind of store can hold text chunks, image embeddings, a decaying knowledge graph, relational metadata, and raw source bytes — and serve one query across all of them?
That's the role PerceptDB plays under Miriel. Percept is a multimodal data cloud: vector indexes, SQL tables, event and time-series tables, a native knowledge graph, and object storage live in one platform, with the original bytes and their derived records kept connected. When learn() ingests a document, its chunks, vectors, extracted entities, and source file don't scatter across a vector DB, a graph DB, a Postgres instance, and an S3 bucket that you then federate at query time. They land in one system that was built to be asked questions spanning all of them — which is exactly why a single query() can range over a contract PDF, a screenshot, and a support-ticket export without you writing a line of integration code. Documents today; the same substrate handles video, audio, and live event streams, so the reach of one query grows without the API growing new verbs.
The bet, restated#
CRUD asks you to predict your questions and encode the predictions as schema, then charges you a migration every time you were wrong. learn() and query() make the opposite bet: take data exactly as it exists, let the structure be computed — and recomputed — from the data itself, and let questions range over everything at once.
Two verbs, not four. Not because ingestion and retrieval got simpler, but because the hard part in the middle — deciding what your data means, how it connects, and how a question should be answered — moved into the routers. You bring the data and the questions. The schema, and the query plan, are no longer your job.


