← Blog

Chat agents

Law-firm intake agent: a beroepsgeheim-first playbook

A 27-person Tilburg practice handles 1,920 intake questions a week across Kleos and SharePoint. Here is the playbook for an agent that triages them without touching a privileged token.

Jacob Molkenboer· Founder · A Brand New Company· 16 Jun 2026· 9 min
Cream wax-sealed envelope with brass key and green silk ribbon on ivory desk, leather ledger behind, deep shadow.

The intake desk at 09:14 on Monday

The firm's senior paralegal opens her queue at 09:14. There are 312 intake-vragen waiting from the weekend, sent through the website form, the algemeen email address, and the after-hours voicemail transcripts. Most are routine. Eleven mention a name that already shows up in an active conflict-search. Three describe situations that read like privileged disclosures from someone who is not yet a client. She has roughly forty minutes before the first partner stand-up.

That was the situation we walked into at a 27-person Tilburg practice group. Family law, employment, and ondernemingsrecht under one roof. Around 1,920 intake-vragen a week across the three desks. A 13-year-old Kleos dossier system holding client and matter records, and a SharePoint on-prem share with the historic correspondence. The brief was direct: cut intake-triage time roughly in half without putting one privileged token on a path that could write to the cliënt-portaal.

This is the playbook we shipped. It is opinionated, it is small, and the constraints did most of the design work for us.

Four constraints that shaped everything

Before the first line of code, we wrote four constraints on a whiteboard. The partners would not let us trade any of them.

  1. Beroepsgeheim is absolute. Article 11a Advocatenwet treats privileged information as something the lawyer cannot share, full stop. The bar's own guidance on cloud and AI repeats the rule and tightens it for hosted services. The NOvA innovation and ICT pages are the current reference.
  2. No autonomous writes. Nothing the agent decides reaches a real dossier, a real client, or the portal without a partner clicking approve.
  3. Kleos stays untouched. No schema migration, no plugin, no upgrade window. The 13-year-old install is load-bearing and nobody wants to be the person who broke it the week before vacation.
  4. The model never sees a full dossier. Only the minimum slice needed for the triage decision in front of it. No PDF bodies, no email threads, no scan-of-paspoort.

Those four lines ended up determining the deployment target, the prompt scaffolding, the queue design, and the audit trail. We re-read them every time a tempting shortcut showed up later.

Mapping Kleos and SharePoint without touching the schemas

Kleos exposes a read-only ODBC channel that a lot of firms forget exists. SharePoint on-prem 2019 has a perfectly serviceable REST API once you stop fighting it. We built a thin read-only adapter for each, behind a single internal endpoint that the agent could call. The agent never sees the connection strings.

@router.get("/intake/context")
def intake_context(query: str, requester_email: str):
    # 1. Conflict check against Kleos relations.
    hits = kleos_ro.search_relations(query, limit=5)

    # 2. Pull only matter metadata, never the body.
    matters = [kleos_ro.matter_card(h.matter_id) for h in hits]

    # 3. SharePoint: titles and last-modified only, no bytes.
    docs = sp_ro.search_titles(query, top=10)

    return {
        "conflict_hits": hits,
        "matter_cards": matters,
        "doc_titles": docs,
        "requester_in_kleos": kleos_ro.is_known(requester_email),
    }

The agent never receives a dossier body. It receives matter cards (name, status, lead partner, opened-on) and document titles. That alone covers around 80% of triage decisions: which desk, which partner, is this a conflict, is the sender already a client. The 20% that needs document content goes straight to a human; the agent's job is to recognise that and stop.

The triage pass

Every incoming intake-vraag runs through a three-step classifier before any human sees it. The classifier itself is a small instruction-tuned model running on the firm's own hardware, in a server room two doors down from the partners. We deliberately avoided sending privileged content to a hosted endpoint. For a Dutch law firm the calculus is sharper than for a general business. It is not a preference, it is the difference between a defensible policy and a tuchtklacht.

The three steps:

  1. Classify the desk. Family, employment, ondernemingsrecht, or "needs a human to look." The model gets the message plus the requester's status (known client, former client, unknown) and returns one label plus a confidence score.
  2. Run the conflict probe. The agent extracts names, KvK numbers, and addresses, then calls the Kleos read-only endpoint. Any hit above a similarity threshold parks the question in a "conflict review" lane and stops further routing.
  3. Flag the beroepsgeheim cases. If the message looks like a privileged disclosure from someone who is not yet a client (this happens more than people realise on intake forms) it gets a hard flag and a partner-only routing.

Output is a structured object the queue understands. No free text, no draft reply, no "here is what I would say back."

{
  "intake_id": "iv-2026-06-14-0231",
  "desk": "employment",
  "desk_confidence": 0.93,
  "conflict_hits": [],
  "beroepsgeheim_flag": true,
  "requester_status": "unknown",
  "suggested_partner": "MdV",
  "rationale": "Sender describes an ongoing internal investigation at a named employer; no prior matter on file."
}

The rationale field is the one piece of generated text in the whole pipeline. The partners use it the way they would use a junior associate's summary, and they override it freely. Every override gets logged.

The beroepsgeheim gate

This was the part the partners wanted to design in the room with us. The rule we settled on is the simplest one that holds: any record with beroepsgeheim_flag: true is forbidden from leaving the on-prem network, forbidden from being summarised in any digest email, and forbidden from being shown to anyone other than a named partner or the designated paralegal on that desk.

Technically that meant three things:

  • A separate database table for flagged records, on a separate volume, with row-level access control keyed to AD groups.
  • A redaction pass on every downstream surface (dashboards, search, even error logs) so a flagged record literally cannot render outside its allowed audience.
  • A daily reconciliation job that walks every system the agent touches and checks that no flagged ID has leaked into an unauthorised table or log.
Warning

If flagged records share storage with normal records and you rely on application-layer filtering, the first developer who writes a quick admin query will leak them. Physical separation is cheaper than the tuchtklacht.

The partner-approval queue

Every triage decision lands in a queue, not a mailbox. The queue is the only surface where a human ever interacts with the agent's output, and it is also the only place where anything can be approved for downstream action.

The interface has four columns, and a record can only move left to right.

  • Inbox. Fresh triage results, sorted by desk and confidence. Anything below 0.75 confidence shows the rationale expanded by default.
  • Conflict review. Anything the conflict probe flagged. A partner has to clear it before it moves on.
  • Privileged. The beroepsgeheim queue. Only named partners can open these records, and the audit trail records every open.
  • Approved for client portal. The exit door. Records here are eligible to trigger a write to the cliënt-portaal: dossier creation, intake email, calendar slot.

There is no auto-approve shortcut for high-confidence records. The agent's job ends at "suggested action plus rationale"; a human's job begins there. The partners liked this because it mirrored the way they already train junior associates, and it gave them a defensible answer for the supervisory question "who decided this."

The write path stays human

The cliënt-portaal sits behind a single write endpoint. The agent has no credentials for it. The queue does, but only after a partner clicks approve. Every write carries the queue record ID, the approving partner's signature, and a timestamp, and it appears in Kleos as a normal opened-by-user action so the audit trail looks like every other action in the firm.

def open_matter(approved_record):
    assert approved_record.approved_by_partner is not None
    assert (approved_record.beroepsgeheim_flag is False
            or approved_record.approved_by_partner in PRIVILEGED_PARTNERS)

    return kleos_write.create_matter(
        client_email=approved_record.requester_email,
        desk=approved_record.desk,
        opened_by=approved_record.approved_by_partner,
        source="intake-agent-v1",
        notes=approved_record.partner_notes,
    )

The agent never calls this function. The queue does, after the two assertions pass. We discussed letting the agent call it for routine matters and the partners rejected that idea within thirty seconds. The right answer was the one they had already arrived at without us.

What six weeks looked like

By the end of week six the numbers had settled into a steady pattern. The paralegal team's intake triage time dropped from roughly 47 minutes per 100 questions to 19. The agent's desk-routing accuracy held at 94% measured against partner override. The conflict probe caught two near-misses in the first month that a human reader would plausibly have missed (both involved former opposing parties operating under different company names). Zero records crossed the beroepsgeheim boundary, verified by the daily reconciliation job and a small external audit at week four.

Two things surprised us, and both were boring and useful. First, partners approved things faster than we expected, because the rationale field meant they were reviewing a decision rather than making one from scratch. Second, the agent improved over time without any retraining, because partner overrides were logged and fed back into the prompt as few-shot examples on a weekly cadence. No fine-tune, just better in-context priors.

The reconciliation job earned its keep in week three. A developer (one of ours, embarrassingly) added a debug log line that included the full triage object, flag field included, to a rotating file outside the privileged volume. The job caught it the next morning, opened a ticket, and the line was gone before lunch. That is the kind of catch you want from a boring cron, not from an auditor.

The five-minute audit you can run tomorrow

If your operation is anywhere near this shape, the smallest thing you can do this week is map your own write paths. Open a notepad. Write down every system where an agent (or any automation, including a Zapier you forgot about) could create, edit, or send something a client will see. Next to each one, write the single named human who has to click before that write happens. If any cell is blank, that is where you start.

When we built the intake-agent for this Tilburg practice the hardest part was not the model or the Kleos adapter. It was getting consensus on the four queue columns, and we only got there by spending the first week sitting in on real intake reviews instead of writing code. If you want the same shape for an intake flow inside your own firm, that is the kind of AI agents work we do.

Key takeaway

If your agent only needs metadata to decide, only give it metadata. Bodies and beroepsgeheim are a separate, much harder permission.

FAQ

Can the agent send anything to a client without a human in the loop?

No. The agent only produces a structured triage object plus a rationale. Every write to the client portal, dossier system, or email goes through a named partner clicking approve in the queue.

Why a local model instead of a hosted one?

Beroepsgeheim under Article 11a Advocatenwet treats privileged information as something a lawyer cannot share. Sending intake content to a hosted endpoint creates a defence problem that on-prem inference avoids.

Did you have to upgrade or modify Kleos?

No. We read through the existing read-only ODBC channel and never wrote anything back through the agent. All writes go through the firm's normal Kleos write path, signed by the approving partner.

How is the beroepsgeheim flag actually decided?

The classifier looks for disclosure language from non-clients, sensitive third-party mentions, and a small set of explicit triggers agreed with the partners. Anything ambiguous defaults to flagged, not unflagged.

What stops the agent from hallucinating a wrong routing?

The agent never drafts a reply or opens a matter. It only proposes a desk and a partner with a confidence score and a rationale. Low confidence expands the rationale automatically so the partner sees the reasoning before approving.

chat agentsai agentsprocess automationintegrationsarchitecturecase study

Building something?

Start a project