Case study · Granted Health · founding team · 2025–now
Case Automation
I designed and lead the agent platform that resolves a patient's medical-billing case at Granted Health. A case is a long-running, adversarial process: documents arrive out of order, deadlines are real, and a wrong action costs the patient money. So the interesting engineering isn't the model call. It's everything wrapped around it.
Multi-agent workflows · event-sourced state · human gates · durable execution · eval harness
One case, end to end
The architecture is abstract until you follow a single denial through it.
- 01
A denial letter arrives
The read pipeline parses it, classifies the denial, and extracts the facts that matter: codes, amounts, deadlines. Every extracted fact carries its source.
- 02
Facts land in the store
Each one is an event, not a row update: what was learned, where it came from, how much to trust it. The case's history is replayable from the log.
- 03
The planner wakes up
It reads the case through the context engine: the relevant playbook, the current gate state, the facts. It never sees one giant prompt.
- 04
It proposes exactly one action
Say, request an itemized bill from the provider. The proposal is a typed object, not prose: action, arguments, preconditions, an idempotency key.
- 05
Code checks the preconditions
Is the deadline still open? Did we already ask? If a precondition fails, the proposal dies before anything happens.
- 06
A person approves the consequential ones
Sending an appeal or contacting a payer waits for human sign-off. The reviewer sees what the agent knew and why it proposed the action.
- 07
The executor acts, once
Idempotency boundaries mean a retry can't send two appeals. The result is appended to the fact store as a new event, and the loop continues.
- 08
The whole run is traced and evaled
The same graph boots against seeded cases with side effects suppressed. Regressions show up as changed decisions, not changed sentiment.
The seam between model and world
The design reduces to one contract. The model reasons; code decides what reasoning is allowed to touch.
// The seam between model and world. The planner can only
// return one of these; everything else is enforced in code.
type ProposedAction = {
kind: "request_itemized_bill" | "draft_appeal" | "message_patient" | /* … */;
args: Record<string, unknown>; // validated against the action's schema
basis: FactRef[]; // which facts justified it, by event id
preconditions: Check[]; // evaluated in code before execution
approval: "auto" | "human"; // consequential ⇒ human
idempotencyKey: string; // a retry can never act twice
};Failures, and what catches them
Every mechanism above exists because something broke without it.
| Failure we hit | What catches it now |
|---|---|
| Instruction X quietly degrades instruction Y as the prompt grows | Context registry: the agent reads what it needs per task; the prompt stays flat |
| The agent re-asks and contradicts itself across turns | Event-sourced fact store: one source of truth, with provenance |
| You can't tell what the agent decided, let alone enforce it | Planner/executor split: every decision is a typed, logged proposal |
| A retry fires the same action twice | Idempotency keys at the executor boundary |
| An ambiguous document produces a confident wrong answer | Retrieval answers carry scope; ambiguity escalates to a person |
| A prompt change silently regresses last month's cases | Evals boot the real graph on seeded cases and score decisions |
Why it's shaped this way
The shortest honest account is the sequence of failures. Margin notes carry the deeper reason or the paper behind a move.
One prompt, one call
The simplest agent is one model call per turn: hand it the case, a set of tools, and a prompt telling it what to do. It holds up until you add the second capability, and the third. Each new rule expands the prompt, and at some point adding instruction X quietly degrades instruction Y.Not anecdotal: “never do X” rules measurably stop being followed as a conversation runs long (Constraint Decay, arXiv 2604.20911), while “always do X” rules hold near 100%. A bigger prompt is a worse prompt. So the first move is to stop stuffing it: the agent reads context on demand from a registry (the relevant playbook, the current gate state) instead of carrying all of it at once.Close to what Agent-S (2503.15520) calls a global action repository, and to writing prompts as named, ordered routines (2501.11613). The prompt stays small and flat as the system grows.
Move the facts below the model
Now it reads context, but the facts of the case still live in the conversation, so the agent re-derives what it already knew every turn. It re-asks, contradicts itself, drifts. The fix is to move state below the model: a canonical, event-sourced fact store where each fact carries where it came from and how much to trust it.The pattern recent work calls event sourcing for autonomous agents (2602.23193), and the memory/compute split behind DeepSeek's Engram: facts become a lookup, not a recomputation. The agent reads from one source of truth instead of reconstructing it each time.
Separate deciding from doing
The agent still decides and speaks in the same breath: the action is just whichever tool it happened to call. You can't enforce a precondition in prose, and you can't even measure what it decided. So separate proposing from doing. A bounded planner proposes one action in a short, read-only loop; a single executor carries it out; preconditions are checked in code; and anything consequential waits for a person.The tempting wrong turn is to make the agent “reliable” by rebuilding it as a deterministic state machine. That just relocates the non-determinism into the resolver and the queue. Keep the agent; enforce the guarantee at the source.
Make failure visible
None of this is real until you can catch it breaking. Tool-match and similarity scores miss the failures that matter: asking to escalate twice, inventing a fact. So the eval boots the real execution graph against seeded data with side effects suppressed, and scores the decision the agent made, not the words it said.The simulacrum idea behind τ-bench (2406.12045); on methodology, Anthropic's Demystifying Evals (pass^k over whole trajectories).
Built on the Vercel AI SDK and Claude, with Langfuse for tracing, Vellum for retrieval, an event-sourced fact store on Gel, and durable execution on Temporal. Ask my agent about any of it on the home page.