By Satoshi Imai ; Finatext Ltd. What I set out to support, how I built it on AWS, and the architectural philosophy that emerged along the way. Contracts are living things It’s tempting to think of a contract as something you sign once and file away. In practice, a contract is alive. It gets amended. Deadlines shift. New people join the project and need to get up to speed. Renewal windows open and close. Across the whole lifecycle, the people who need to act on a contract — in sales, in finance, in the legal team that supports them — all need to be looking at the same, current picture. And that’s genuinely hard work. Not because anyone is doing anything wrong, but because it’s an inherent coordination challenge of the domain. The more an organization grows, the more contracts and stakeholders it has, and the more effort it takes to keep everyone aligned on the latest state of each one. This is true for any company and its partners alike — it’s just the nature of contract lifecycle management. So here’s a nice thought to build toward: what if the current state of every contract — its terms, its upcoming milestones, the status of what’s in flight — were something everyone involved could see, the same way, whenever they needed to? That kind of shared, always-fresh picture is exactly what lets both sides of a deal fulfill their contracts smoothly and with confidence. Less time spent chasing status, more time spent on the relationship. That’s what I built ContractOps to support. Not with a single clever prompt, but with a deliberate, down-to-earth architecture. This article is a tour of how I built it — on AWS, fully serverless — and the design philosophy I kept returning to at every turn. What ContractOps actually is Since this article is meant to be the canonical description of the system, let me draw the boundaries clearly before the tour begins. ContractOps is a multi-agent system that supports Legal Operations — the contract and billing administration that a company’s sales and finance teams carry out, with the backing of the legal department. It is deliberately not a system that renders legal judgments or performs legal work on its own. The work the AI does is bounded and concrete: it structures, indexes, searches, and keeps watch over contract-related information, it assists with drafting (producing first drafts and edits for people to review), and it prepares the groundwork so a human can reach a decision faster. The final judgment always belongs to a human — and, for anything carrying legal weight, to the legal team or a qualified professional. Human-in-the-loop isn’t a feature I bolted on at the end; as you’ll see, it’s wired into the data model itself. The system is a small pantheon of single-purpose agents, named after Greek deities — less for flair than because a good name makes a responsibility easy to remember. What matters here is that they fall into three layers: The people-facing layer. Themis is the Slack-native assistant people actually talk to — search, analysis, Q&A, and draft assistance — turning information that’s normally scattered across places into something you can simply ask about . Iris is the gateway in front of it, validating incoming Slack events and routing them by domain. The monitoring layer. A small fleet — Argus , Chronos , and Cassandra — that keeps the shared picture fresh, each from its own angle. (They’re the stars of Decision 7, so I’ll save the detail.) The knowledge layer. Behind both runs a data pipeline of specialist agents I call the Scribes — the ones who turn raw documents into trustworthy, structured knowledge. You’ll meet them by name on the tour, exactly where their work comes up. One thing holds for every agent in the system: it reconciles facts, structures information, and notifies. None of them makes a legal call. Chronos can say “this milestone is coming up”; it never decides what should be done about it. That decision is a person’s. The substrate is fully serverless on AWS : AWS Lambda for compute, Amazon DynamoDB for state, Amazon Bedrock (Claude Sonnet 4.5) for inference, Amazon EventBridge for scheduling, Amazon S3 for intermediate artifacts — with Slack and Google Workspace integration at the edges. There is no external message queue. That omission turns out to be a feature, not a gap, and I’ll get to why. ContractOps system architecture (AWS + GCP) ContractOps at a glance: a fleet of single-purpose Lambda agents over a DynamoDB single source of truth, with model inference kept inside the AWS boundary. Now, the tour. Each stop is a design decision, framed the way I actually faced it: a problem anyone building serious LLM agents will hit, the call I made, and what it cost. A principle that kept coming back Before the specifics, one idea connects all of them, so let me name it. An LLM is a probabilistic reasoning engine . The physical data layer — file hierarchies, spreadsheet coordinates like A1:Z45, raw database schemas — is deterministic structure . When you let the two touch directly, the model ends up spending its reasoning on the wrong job: guessing where data lives and how to address it, instead of thinking about your actual question. The fix isn’t novel. It’s a return to principles software engineering settled decades ago — encapsulation, separation of concerns, the Information Expert principle (give a responsibility to the component that actually has the knowledge to fulfill it). Business logic was never asked to understand the physical layout of a database; the industry put a data-access layer in between. An LLM deserves the same courtesy. The further I went, the more this hardened into a full architectural paradigm, which I’ve since written up as a preprint — [ The Survey-Sonar-Pickup (SSP) Paradigm: Redefining Responsibility Boundaries Between LLMs and Physical Data Layer ] . You don’t need the theory to follow this article; you need the decisions it produced. Here they are. Decision 1 — Don’t send the model exploring. Hand it a menu, not a map. The universal problem. The seductive shortcut in agent design is to grant the model broad access — “here’s read access to the database, go find what you need” — and trust its reasoning for the rest. Modern models are good enough to attempt this, which is exactly the trap. With no map, the agent resorts to trial and error: guessing table names, guessing column meanings, generating speculative queries. Tokens evaporate, the context window chokes on its own speculative garbage, and your system collapses into a spectacular, expensive hallucination loop. The approach. Revoke the model’s right to free exploration. Instead of accepting open-ended search against the physical layer, the tool presents a closed menu of logical options — semantic IDs plus just enough metadata to choose between them. The model’s job collapses from open-ended generation (“write a query to find X”) into bounded selection (“pick from these”). Hallucination has nowhere to go, because every option on the menu provably exists. The trade-off. The moment you take away free exploration, you owe the model completeness . The menu must exhaustively cover what’s actually there — a candidate missing from the list is one the model can never know to ask for. Drilling down a hierarchy is fine (folders, then files within a chosen folder). Forcing the model to page sideways through “next page, next page” because one response was scoped too narrowly is not. Decision 2 — Pay the comprehension tax up front, not at runtime. The universal problem. Chatbot-era design is poisoned by the Zero-Latency Dogma — the absurd belief that a tool must answer in a second or two or it has failed. But genuinely understanding enterprise documents is expensive . A scanned contract needs OCR and layout reasoning. A richly formatted spreadsheet can encode meaning in cell colors and borders. Done properly, that takes real time — so, under pressure to respond instantly, tools skip the hard part and return whatever flat text they can grab in the moment. That’s how meaning gets dropped before the model ever sees it, with nothing to signal the loss. The approach. Move the heavy structural work off the runtime path entirely and do it asynchronously, ahead of time. In ContractOps, an upfront ETL pipeline absorbs the physical noise before the model is ever involved. Whether the source is a clean digital PDF or a skewed paper scan, the Scribes homogenize it into one rigorous semantic tree — articles, sections, items — expressed as structured Markdown. The model never spends a token interpolating a smudged character or guessing at a layout. The LLM works here too, but as the eyes of a deterministic ETL process , not as a live explorer. The payoff: by query time, the data is already known territory . The latency-versus-completeness dilemma that feels impossible at runtime simply dissolves, because the cost was paid in advance. The trade-off. It looks like you’re trading away instant freshness on brand-new data — but that’s an illusion. The “instant” alternative only ever handed the model impoverished flat text, so there was never any real freshness to lose. And data that genuinely is brand-new — dropped in mid-conversation — gets its own runtime path (Decision 5). The one honest cost is the engineering effort up front: the pipeline has to be designed and maintained. In return, the model can’t be caught off guard by structure that nobody mapped. On AWS. This is a textbook serverless ETL. EventBridge fires on a schedule → Clio (a lightweight Lambda) routes the work → Metis dispatches each document to Theia (vision OCR) or Eunomia (text structuring), both calling Bedrock → intermediate Markdown lands in S3 → structured results land in DynamoDB. Heavy, binary-dependent steps run in container-based Lambdas; everything else ships as a slim ZIP. Idle cost is essentially nothing, because nothing runs when nothing needs to. Clio data pipeline: AllocationMap → contract analysis The pre-runtime comprehension pipeline. Physical noise is absorbed asynchronously and turned into structured, queryable knowledge before any agent asks a question. Decision 3 — Split “finding” from “fetching” at the storage layer. The universal problem. Suppose your data is already beautifully structured. You drop it into one searchable store and let the agent query it. The first hit comes back — and drags a 40-page contract along with it, because the store doesn’t distinguish “the thing you searched for” from “the entire payload of that thing.” A couple of hits like that and the context window is gone. One careless search, total collapse. The approach. Separate the act of finding from the act of fetching , physically, into two stores with two jobs: A search index (the ContractOps table) holds only ultralight metadata — parties, execution dates, status, contract type. The agent scans this to grasp the landscape, with near-zero pressure on the context window. An extraction store (the ContractCodex table) holds the actual payloads — the structured clauses themselves. The flow becomes deterministic: the agent reviews the lightweight menu, picks a document ID and a clause key, and only then fetches exactly those clauses by ID. What used to be an uncertain similarity search across a giant text blob becomes a precise, two-step API call. Searching and reading are no longer the same operation, so reading can never accidentally flood the model. On AWS. Both stores live in DynamoDB under a single-table design, but their responsibilities are cleaved apart by purpose-built tables and secondary indexes — operational metadata indexed for the “find” path, contract knowledge partitioned for the “fetch” path. Same database technology; deliberately different access surfaces. Decision 4 — Don’t hide what you can’t fully trust. Make trust a column. The universal problem. AI-extracted data leaves you with two bad options. Trust it uncritically, and an OCR misread can propagate downstream unnoticed. Or gate every extracted clause behind human review before anyone can use it — honest, but it doesn’t scale. The approach. A third path: don’t block the data, grade it. A dedicated auditor agent, Dike , runs as the last step of the ETL pipeline — long before any model does semantic reasoning. It deterministically cross-checks the structured output against the raw text (using techniques like SHA-256 hashing), marks the indisputable matches as trusted, and marks anything with detected drift as requires verification . Crucially, it does not remove the uncertain data. Everything stays searchable; only its trustworthiness becomes explicit metadata — a “trust boundary” baked right into the schema. This is what it means to treat the AI’s own output with zero trust . Neither the model nor a person ever has to take a clause on faith. They can see, per item, exactly where automation can be relied upon and exactly where a human should look. And this is where Human-in-the-loop actually lives. It isn’t an abstract principle — it’s a flag in a table that says, for each piece of data, “from here, a person decides.” For a Legal Operations tool, that line — drawn by the system, in data, between what’s safely automated and what a person (and where appropriate, the legal team) should review — is the whole point. The trade-off. The cost is a deterministic audit pass over every clause, paid up front — and it’s a bargain. In return you get two things at once. You structurally prevent the kind of undetected error that no after-the-fact warning can ever catch; and every output now carries an explicit, machine-readable trust boundary. A risk that used to be unknowable — contamination that raises no error of its own — becomes a known, bounded property of the data that humans and agents alike can act on. Decision 5 — Match the pattern to the nature of the data. Reject the universal solution. This is the heart of the tour. Not all data is the same shape, and the single biggest architectural mistake is pretending it is — trying to serve everything through one generic file-reading tool. ContractOps recognizes three fundamentally different kinds of data and applies a different decomposition to each. A. Settled data — already in the system. Past, executed contracts that have been through the ETL pipeline. The comprehension was done up front, so the runtime tooling is lean: scan the lightweight index, then fetch by ID. This is the world of Decisions 2–4. B. Just-arrived data — handed over in the moment. A draft a user just dropped into the chat; a working spreadsheet that didn’t exist five minutes ago. There’s no pre-processing something you’ve never seen, so here the parsing does happen at runtime — but carefully. A heavyweight, isolated tool reconstructs the document into structured Markdown via vision reasoning, then returns just the list of section headers — a table of contents that doubles as a schema. The moment those headers exist, the model is no longer wandering an unknown document; it’s choosing from a map. It then issues one of two deterministic requests: fetch a whole section, or pull only the blocks matching a condition. The tool runs the repetitive search internally and answers in one shot — declarative extraction , instead of the model groping item by item. C. Opaque external data — out in the wild (think an untamed Google Drive: folders of mixed PDFs, scans, and docs nobody curated). Files scattered across storage you don’t control and can’t pre-index, where the search API hands you discovery and filtering as a single inseparable operation. Here I keep “finding” fused but force “fetching” to stay separate and lazy — never opening a file until the model has committed to it. The point isn’t the taxonomy for its own sake. It’s the discipline: a tool is not a one-size-fits-all pipe. Choosing the decomposition that fits the data’s actual structure — its maturity, its time constraints, its opacity — is the design work that keeps the model out of trouble. Themis & supervisor agents flow (interactive + monitoring) The interactive side: Themis reasons over pre-structured knowledge, while the supervisor agents watch the same source of truth from different angles. Decision 6 — Choose an honest limit over false confidence. The universal problem. When the agent searches opaque external storage and hits a file it can’t parse — a legacy Office document, an unreadable image — the tempting design is to filter it out of the results. And even when no one decides this on purpose, simply wiring up generic tools tends to produce exactly that exclusion by default. Cleaner, right? Except now the agent sees nothing where something exists, concludes “no relevant information found,” and reports that with total confidence. A confident false negative — “that document doesn’t exist” when it very much does — is not just a bug; it is an architectural betrayal. The approach. Don’t hide the unprocessable. The search results include the file even if it can’t be opened. The block happens later, at fetch time, where the tool refuses the unsupported format and says so : “a relevant file exists here, but the system can’t process this format — please check the source directly.” The model stays fully aware the file exists, and instead of inventing an absence, it takes the honest path: it escalates to a person with a precise pointer. This is the point that’s easy to get backwards. When the system honestly reports what it cannot read, it makes the human’s proper role possible: you go to the source and, where it matters, cleanse or contextualize that file by hand before handing it back. On honest information, that isn’t a workaround — it’s the legitimate division of labor between a person and a tool. Real collaboration is built on understood constraints, not on the pretense of unlimited capability. Decision 7 — Govern by choreography, not by a conductor. This is the technical centerpiece, and it’s where the serverless substrate really earns its keep. It’s also where ContractOps does its most valuable supportive work: keeping the shared picture fresh. The universal problem. The default way to coordinate many agents is to appoint a central orchestrator and run everything through an external message queue. It works, but it buys you a single coordination point to keep alive, state duplicated between the queue and your database, and a whole category of distributed-systems bugs — most painfully, notifications that drop or double-fire with nothing to flag either. For supportive monitoring, where the entire value is keeping everyone in sync , a missed or duplicated notification is exactly what you can’t have. The approach, in three parts. A. Choreography over orchestration. There is no conductor. The monitoring agents — Argus (contract changes), Chronos (deadlines and milestones), Cassandra (early warnings when activity looks likely to run against the terms) — are independent, each woken by its own EventBridge schedule or event. They never call a central brain. Instead, each watches the same single source of truth — the DynamoDB state — from its own angle. Good coordination emerges from several independent observers cross-checking one shared reality, the way a well-run team stays aligned precisely because more than one person is paying attention. This is also how the supportive jobs map cleanly onto the lifecycle: Argus helps the latest contract state reach the right people; Chronos surfaces milestones early enough to plan around; Cassandra raises an early warning when planned human activity looks likely to run against the schedule or the agreed terms. B. Pure-state idempotency, with no external queue. This is the part I’m proudest of. No SQS, no broker — just Lambda and DynamoDB. Coordination is achieved through DynamoDB conditional writes (optimistic concurrency) plus disciplined state management. Each agent, before it acts, checks the current state and skips if it isn’t what it expected. The result is a fully idempotent “singleton” flow whose design mantra is: a notification is never missed and never duplicated. Those are the two failure modes that plague every naive notifier, and they’re closed off purely through state, not queue plumbing. C. Self-healing on the assumption of undetected failure. Distributed systems fail in ways that raise no error, so the design assumes it. Every Scribe, whenever it runs, also sweeps for stalled jobs — anything stuck past a short threshold — and resets them to the start of the pipeline. I call it the Ride-Along Sweep : an agent’s ordinary execution doubles as a recovery pass for the stuck work of the colleagues it collaborates with , so a group of collaborators heals itself. Clio runs a full-fleet sweep on a daily schedule as a backstop. The pipeline doesn’t promise it never stumbles; it guarantees it never stays down . A boundary worth naming. Note precisely what these agents do: they reconcile facts and raise notifications. They do not make legal calls. The monitoring fleet hardens the factual groundwork that a person then judges — which is exactly the line a Legal Operations tool must hold. The trade-off. You accept eventual consistency and a little sweep latency. In return you get operational simplicity, low cost, and a kind of fault tolerance that a queue-centric design would charge you dearly for. Decision 8 — Keep secrets inside the boundary. Hold no keys. Contract information is confidential, which makes security its own design pillar — and another place the AWS foundation does heavy lifting. The universal problem. Two fears dominate: that confidential data slips out to a public service or an external AI through some unexpected egress, and that a static credential leaks and hands an attacker the keys. The approach. A private, enclosed boundary. Model inference (Bedrock) and the core retrieval stay inside AWS . Only the strictly necessary paths out — to Google Drive, to Slack — are opened, each behind a tightly scoped API. Unexpected egress is designed out, not merely monitored. Data is encrypted at rest with AWS KMS , and the handful of secrets that do exist live in AWS Secrets Manager — never in code or environment dumps. Keyless security via Workload Identity Federation. There are no static cloud credentials to leak, because there are none. Starting from an AWS IAM Role , the system federates into Google Cloud service accounts by exchanging short-lived tokens on demand — a multi-cloud privilege chain with nothing long-lived to steal. Dual service-account partitioning. The service account that parses documents and the one that searches are strictly separated, so least privilege is enforced at the infrastructure layer, not by convention. Layered Slack access control. Three gates in series: AWS WAF at the perimeter, Slack request-signature verification for authenticity, and a channel-ID allowlist for authorization — logically sealing the public endpoint down to specific channels only. The throughline is the same one running under every decision in this article: explicit limitation. A boundary you can name and enforce beats a capability you merely hope behaves. The philosophy underneath: autonomy through constraint Step back, and every decision rhymes. Each one takes something away from the model — the right to roam the physical layer, the burden of guessing at structure it was never told about, the temptation to answer when it shouldn’t — and hands that responsibility to a part of the system actually equipped to bear it. What’s left for the LLM is the thing it’s genuinely good at: reasoning over clean, trustworthy, bounded options. That turned out to be a paradigm worth formalizing. The grand version of the idea — why these boundaries are not just convenient but necessary , and how to reason about them systematically — is laid out in a preprint I’ve written: Survey–Sonar–Pickup (SSP) . ContractOps is its living proof, but the principles are meant to outlive any one system. And the principle, in the end, is a humble one. Real autonomy — and real collaboration between people and AI — doesn’t come from dogmatically worshiping an LLM’s unconstrained capabilities. It comes from ruthlessly enforcing honest, structured boundaries, so the system knows its own limits and a person always holds the decisions that matter. For a tool that supports Legal Operations — work that belongs to people, backed by their legal team — that isn’t a compromise. It’s the entire point. It’s also what lets everyone involved, on both sides of a contract, get on with fulfilling it in confidence. The architectural paradigm behind these decisions — Survey–Sonar–Pickup (SSP) — is described in a preprint: [ The Survey-Sonar-Pickup (SSP) Paradigm: Redefining Responsibility Boundaries Between LLMs and Physical Data Layer ] . ContractOps runs fully serverless on AWS (Lambda, DynamoDB, Bedrock, EventBridge, S3). If you’re building agents that work with messy, high-stakes information, I hope a few of these boundaries are worth borrowing. Special Thanks: By the way, the awesome agent icons used throughout ContractOps (and in the architecture diagram here) were generated by Gemini (nano-banana). In the spirit of full transparency — and recognizing the irony of writing an entire article about strictly limiting AI, only to fully outsource the graphic design to it — huge thanks to the model for bringing these agents to life! ContractOps: A Serverless Multi-Agent System for Contract Operations was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.