Court forms on Lawhive are filled by a background pipeline the lawyer
can't see. We're moving that work into Lawrence's chat loop: the form fills in front of
you, every value carries a citation to the case material it came from, and when the case file
has a gap, Lawrence just asks.
By Martin Hartt · Lawyer Experience (LEX)
agents + platform-v3flag-gated · legacy fallback intactstatus: active
FL-100 · Petition
Jane A. Mitchell ✶ Lawrence
14 Jun 2015 ✶ Lawrence
3 Mar 20
I can't find a separation date. When did the parties separate?
3 March 2024.
What the lawyer sees
The lawyer asks for a form in chat and watches it fill in the open FormEditor.
Fields land one collection at a time, each tagged "Completed by Lawrence" for review; when the
case file can't answer something, Lawrence asks in the thread and carries on from the
reply.
legal-os · matter / Mitchell v Mitchell
FL-100 · Petition
Lawrence
The problem
Today, a lawyer on a divorce matter types: "Can you fill out the FL-100 petition for
the Mitchell divorce?" What happens next is silence.
The message fires an Inngest event into a background pipeline and the chat turn ends. The
pipeline fans out across the form's collections, fills each group of fields with one LLM call
per group, and writes everything back as a single document. It's a well-built batch system:
grounded fills, citations verified verbatim against the source material, one atomic write.
The lawyer experiences none of that. They see a loading state for several minutes: no
progress, no partial output, and no way for the pipeline to ask a question. If the case file
never mentions a date of separation, the pipeline can't ask the one person who knows it. The
lawyer finds the blank field during review, long after the conversation that could have
answered it in five seconds has moved on.
A batch system bolted onto a conversation
The legacy pipeline isn't bad; it's the wrong shape. Three structural costs follow
directly from fire-and-forget:
The work is invisible. Nothing reaches the lawyer until everything
is done. A 90%-complete form and a stalled run look identical from the outside.
Questions are impossible at the exact moment they're cheapest. The
pipeline hits an ungroundable field mid-run, when nobody is listening; the lawyer is right
there in chat, on the wrong side of an async boundary.
The context is fetched twice. By the time a lawyer asks for a fill,
Lawrence has usually already read the notes, parties and key documents. The pipeline
re-retrieves all of it, per field group.
Meanwhile, two things became true that weren't when the pipeline was built: Lawrence has a
production edit surface that lands batched field edits as unreviewed "Completed by Lawrence"
changes (shipped for the form-editing skill), and the FormEditor live-merges those edits on a
3-second poll. The expensive parts of "fill the form inside the chat" already exist. What's
missing is a way to start a blank form, and a procedure for filling it honestly.
Move the fill inside the turn
If the agent already holds the case context and already has a write surface
to the live form, the pipeline isn't earning its async boundary.
So: keep the request inside the chat turn. Lawrence reads the form's outline, gathers the
case context it doesn't already have, creates a blank draft the lawyer can see immediately,
and fills it collection by collection. Anything it can't ground gets skipped with a reason
instead of guessed, and the turn ends with one batched set of questions. The diagram puts the
two designs side by side.
The operations Lawrence runs
The same FL-100 fill, but as the sequence of tool calls Lawrence issues inside the
chat turn: the developer's view of the walkthrough at the top of the page. Each step is one
operation against the VFS, with its real request and response. Scroll through the full trace.
Lawrence · tool calls matter mat_8x · FL-100
The design, in five decisions
Most of this project is plumbing that already exists. The decisions below are the parts
that aren't: each one chosen over a real alternative, with the tradeoff named.
1 · A blank skeleton first, on a forms-native VFS scheme
The fill starts with the generic VFS create verb: create
filled-forms://<matter>, the form id in the payload, not a bespoke
start_form tool. A filled form gets its own scheme, distinct from the blank
forms:// definitions it's filled from: the same template-versus-instance split the
VFS already draws between precedents:// and the documents:// they
create. create, read and edit all live under
filled-forms://, so one scheme covers a form's whole lifecycle.
lawrence-api builds the blank FILLED_FORM skeleton from the raw template
annotations, because the forms:// outline deliberately strips PDF metadata: the
agent gets each field's meaning without paying context for its geometry.
Fields it can't handle (widgets other than text or checkbox, types outside the legacy
allowlist) are pre-marked Unsupported at creation, so Lawrence never wastes a batch
discovering them.
The scheme is an addressing layer over today's FILLED_FORM artifact. Whatever
store forms move to later (the field-native model field-level writes need) stays invisible
behind the resolver, and moving the editable surface here leaves artifacts://
fully read-only.
read forms://fl-100
"Petitioner: full legal name of the spouse filing." Meaning only; no PDF ids, no widget types.
→
create filled-forms://<matter>
Server rebuilds the skeleton from the raw template annotations, or returns the open DRAFT if one exists.
→
DRAFT v1 · live in the FE
field_id 2 · text · Incomplete … with Unsupported widgets pre-marked.
The outline carries meaning; the skeleton carries targets. Only the server ever sees both.
Creation is idempotent: an open DRAFT is returned, not duplicated (a partial unique index on
(matterId, formSlug) enforces it). That is also the resume mechanism: a follow-up
turn gets the same draft, re-reads field states, and continues.
2 · Small batches the lawyer watches land
The legacy data model already encodes which fields are independent: collections run in
parallel, groups within them run sequentially because later groups depend on earlier answers.
The agentic loop reuses that seam, but the unit of work is a small batch, not a whole
collection. The skill has the agent fill a few fields at a time, grouping fields that share
reasoning and never crossing a dependency group, under a low hard cap. That is what makes the
fill watchable: fields land a few at a time through the FormEditor's existing poll,
rather than a whole collection appearing at once (which would just reproduce legacy's
all-at-once feel). A large collection is split across several small batches, and each batch
persists a new version.
After each batch:
Inspect the per-edit results; retry a failure once.
Never blind-retry a timed-out batch without re-reading (the write may have landed).
Hard cap of 10 iterations: stop one early and report filled-versus-remaining rather than
truncate silently. A large form takes multiple turns by design; resume is free (decision 1).
v2 · batch: parties
Petitioner, Respondent land applied
→
v3 · batch: marriage
Date of marriage applied · Date of separation skipped
→
v4 · batch: children
No minor children applied
Each small batch is its own version. The lawyer watches a few fields land at a
time through the 3-second poll, not the whole form at once.
3 · Citations and confidence ship in v1, because the slots already exist
"Trust, but verify."
Russian proverb
Every filled value carries a citation and a confidence score, because the slots already
exist: the artifact has per-field citations and confidence, the
FormEditor renders both today (source badges, a low-confidence "Review" flag), and the legacy
pipeline populates them. The agentic path writes the same slots; skipping them would ship a
visible regression.
Each edit takes an optional citations array (the legacy
CitedFieldSource shape, keyed by ❬N❭ markers in the reason) and an
optional confidence ({score, rationale}).
Skill rule: cite only verbatim passages from sources read this turn.
The server checks the citation's shape (type allowlisted, id present), not the
passage. Same trust model as our in-production chat citations; verbatim verification is the
fast-follow.
{ "field_id": "2", "value": "Jane A. Mitchell",
"reason": "petitioner named in intake note ❬1❭",
"citations": [
{ "ref": 1,
"source": { "type": "note", "id": "matnt_4f" },
"passage": { "text": "Petitioner: Jane A. Mitchell" } }
] }
Top: the field as the lawyer reviews it; the ❬1❭ badge opens the
cited source with its verbatim passage. Bottom: the single edit that produced all of it.
4 · Ask at the end, not in the middle
Pausing mid-fill to ask sounds attentive but costs the finished form: it burns loop
iterations and strands half-filled drafts when the lawyer doesn't answer. Instead, Lawrence
skips what it can't ground, finishes the pass, and ends with a summary plus one batched set of
questions.
Date of separation—Skipped · no date in the case file
Lawrence · end of turnWhen did the parties separate?
The Skipped field is the question list: durable on the artifact, answered
in chat, and left visibly Skipped (never re-asked) if the lawyer ignores it.
The questions anchor to the Skipped fields on the artifact, not conversation
memory. Next turn, Lawrence re-reads them, edits exactly the ones the answers cover, accepts
partial answers, and never re-asks what the lawyer ignored.
5 · Whose value wins
Once the lawyer can edit while Lawrence fills (something the legacy pipeline never had to
survive), collisions become real. Three guards, at different layers:
Reviewed field (server): an edit to a lawyer-reviewed field returns
skipped, not an overwrite. A guard clause, not a schema change.
Active edit (client): the FormEditor's poll merge skips any field the
lawyer is mid-keystroke on, so a landing batch can't clobber an unsaved edit.
Different-field race (compare-and-set, v1): the persisted write is
whole-document, so a lawyer save and an agent batch on different fields in the same
~3-second window could revert each other. Each agent write carries the version it read; a
stale write is rejected, and Lawrence re-reads (now holding the lawyer's value) and re-applies
only its fields. Field-level writes retire this hazard entirely; that is the fast-follow.
An agent edit against a reviewed field comes back as a per-edit skipped
result; the lawyer's confirmed value stays exactly as they left it.
lawyer saves
edits Petitioner, a different field; the form is now v5.
→
agent write · read at v4
batch sets Respondent with version: v4 → stale (v4 ≠ v5), rejected.
→
re-read v5, re-apply
Respondent lands applied → v6. The lawyer's Petitioner is untouched.
Compare-and-set: the write carries the version it read, so a stale write is
rejected rather than clobbering the lawyer's concurrent save. Lawrence re-reads and re-applies
only its own fields.
The build, step by step
High level, in dependency order. Phase 1 (platform-v3) is inert until called; Phase 2
(agents) merges flag-off.
Create surface. In lawrence-api, add the
create filled-forms://<matter> resolver: build a blank
FILLED_FORM skeleton from the raw template annotations, idempotent on an open
DRAFT. matter-service already persists the shape.
Move the surface onto filled-forms://. Point the filled-form
read and edit resolvers at filled-forms://<matter>/<id> so create,
read and edit share one scheme. artifacts://…/fields stays as a deprecated
alias only if it's already live behind the flag; otherwise it's a clean rename.
Extend the edit surface. In filled-form-edit-schemas.ts
and filled-form-edit-resolver.ts, accept skip,
citations and confidence per edit, write them through, and return
skipped for a lawyer-reviewed field instead of overwriting it.
Compare-and-set on persist. Carry the version the agent read into the
whole-document write; reject a stale write so a concurrent lawyer save on a different field
isn't clobbered, and have the loop re-read and re-apply.
Client keystroke guard. In the FormEditor's poll merge, skip any field
the lawyer is mid-edit on so a landing batch can't clobber an unsaved value.
Create verb + tool card. Add a create verb to the VFS
client (the filled-forms://<matter> path routes to the create resolver),
returning the artifact id and a per-collection field summary, plus a friendly "Creating the
form" card in the legal-os tool-message components.
Flag the path. In agents/chat/.../agent.py, gate tool
selection on agentic-form-filling (and the skills flag); flag-off keeps the
legacy fill_form tool.
Verify and roll out. Side-by-side parity on seeded matters, then flip
the flag gradually, checking for open DRAFTs before each change.
What could go wrong
The parts we're least sure about, and what we're doing about each:
Fill quality could regress. Legacy runs dedicated per-group prompts
plus an optional judge pass; the agentic fill is one skill-guided loop. We don't get to
assume parity: a side-by-side comparison on seeded matters gates the rollout, and legacy
stays one flag-flip away.
A made-up citation can reach the review surface. v1 trusts the agent
to quote verbatim and checks only shape. Mitigations: the quote-only-what-you-read rule,
server rejection of malformed citations, and the review gate (every value lands unreviewed
with its citation shown at the point of confirmation). Verbatim server verification is the
fast-follow.
Very large forms take multiple turns. The 10-iteration loop cap is
real. We chose honest partial completion plus free resume over pretending one turn fits
every form.
Version history gets noisy. One version per batch is what makes the
fill watchable, but it's roughly one version per collection where legacy wrote one total.
Version coalescing is deferred until the history UX actually hurts.
Rollout: two phases, behind a flag
Phase 1 lands in platform-v3: the create filled-forms://<matter> resolver
and the edit-surface extensions (skip, citations, confidence, the reviewed-field guard, the
compare-and-set), all inert until called. Phase 2 lands in the agents repo (the create verb,
the form-filling skill, the per-turn flag wiring) and merges flag-off. The whole
agentic path sits behind one PostHog flag, agentic-form-filling; it also rides on
the skills flag, since agentic-on with skills-off must fall back to legacy rather than load the
fill path without its procedure. Nothing changes until the flag flips; flipping it back restores legacy untouched.
One rollback drill: a leftover agentic DRAFT must still render after the flag goes off (it
does; same artifact shape), and we check for open DRAFTs before any flip.
After v1
Deliberately out of scope for the first release, in rough priority order:
Verbatim citation verification. The server re-fetches each cited
source and confirms the passage is a substring before persisting, restoring the legacy
guarantee. The open question is where the source fetch lives, since the edit resolver is
deliberately a thin persistence layer with no source access.
Parallel collection processing. v1 fills collections serially so the
lawyer watches a clean sequence; collections are independent (that's why they're the batch
unit), so a large form could fill several at once for speed once the watchability tradeoff
is worth making.
Per-field focus ring. Each edit payload carries the field_ids
Lawrence has committed to next, so the FormEditor can ring the upcoming field rather than
have values pop in. It rides the existing batched refetch (no streaming or websockets needed
for v1); the FE rings the declared fields and clears once they resolve or no follow-up batch
arrives.
Calculated fields via code execution. Legacy has an optional
calculation pass for derived values (totals, date arithmetic); it defaults off. The agentic
version is a sandboxed code-execution tool, so a computed field is the output of a program
rather than a generated guess.
Field-level writes, on a forms-native model. Persist only the fields an
edit changed instead of the whole document, removing the whole-document overwrite hazard
entirely and allowing true concurrent edits on different fields. The interim compare-and-set
(in v1) covers the race; this retires it. It needs matter-service to support partial form
updates, which means a forms-native model storing fields as rows rather than one blob, and
that model is also the home forms need to move off MatterArtifact for good. The
parallel Matter
Artifacts → Matter Documents migration deliberately excludes FILLED_FORM
(its Content/ContentVersion model is shaped for rendered HTML documents, not structured
fields), so it doesn't touch us in v1: forms stay on MatterArtifact now, and this
follow-up is where they get their own model and let MatterArtifact finally be
retired. We'd mirror that migration's additive, reversible, flag-gated pattern when we do it.
Fill metrics. An agentic equivalent of the legacy FormFillRecord row,
so quality regressions show up in data rather than anecdotes.
Proactive fills. Lawrence starts a fill from a trigger (new documents
landing on a matter, a key date approaching) rather than a chat request.
Fold the Fill form button into chat. Today it's a separate non-chat
entry point that fires the legacy pipeline. In future it mirrors how Create from
precedent already works: open and pin Lawrence with a seeded prompt ("Please fill this
form…"), and the agent fills it agentically. That reuses an existing, familiar
interaction instead of inventing one, and leaves a single form-fill path on a matter, not
two.
Decommissioning the legacy pipeline. Once parity holds and the button
is folded in, the Inngest event, the engine fan-out, the workflow-status loader, and the
artifact-generation module in lawrence-engine can go.
What we still don't know
Whether one general fill loop actually matches dedicated per-group prompts on quality.
The parity harness will tell us; our intuition is "close enough on most forms, worse on the
gnarliest ones."
Product call: whether to disable the existing Fill form button
while the flag is on. It's a separate non-chat entry point that still fires the legacy
pipeline, so leaving it live means two parallel fill paths on the same matter (and two ways
to land a form). Needs a product decision before broad rollout.
Updates
What's changed since this plan was first shared, newest first.
19 Jun 2026 · Forms get their own VFS scheme. Adolfo pointed out
that create forms://<matter>/<form>/fill read like a REST path, not a
filesystem one. The create now uses the generic VFS verb on a dedicated
filled-forms://<matter> scheme (form id in the payload), with
create/read/edit unified there and
forms:// reserved for the blank definitions. This mirrors the existing
precedents://-to-documents:// split, makes artifacts://
fully read-only, and gives forms a stable address while the underlying store moves to a
field-native model later.
19 Jun 2026 · Checked against the documents migration. Adolfo
flagged Lukas's Matter
Artifacts → Matter Documents migration. It explicitly excludes FILLED_FORM,
so it doesn't collide with v1: forms stay on MatterArtifact for now. Moving forms
onto their own model (so MatterArtifact can be retired) is folded into the
field-level-writes follow-up, not v1.
19 Jun 2026 · Review feedback and polish. Addressed comments from
Adolfo, Peter and Wills: start_form became a VFS create operation
rather than a bespoke tool; batching moved from a 200-per-collection cap to small,
agent-chosen batches the
lawyer watches land; compare-and-set was promoted into v1 to close the cross-field race, with
field-level writes named as the fast-follow; and a per-field focus ring was added to After v1.
The decisions were also tightened into scannable bullets, with worked figures for the batch
sequence and the compare-and-set conflict.
10 Jun 2026 · Published for review on html-previews, with inline
comments, reactions and approvals.