AI Business Process Mapping Should Output a Graph, Not a Diagram

AI process mapping that holds up: LLM extraction into a typed graph, source spans, measured alias merges, owner sign-off and HL7 event log checks.

Clint JohnsonPublished 12 min read

Key takeaways

  • AI process mapping should output a typed graph with a verified source span on every node and edge; BPMN and swimlane diagrams are views rendered from it.
  • Exact name matching misses aliases that only shared context reveals, so store merges as reversible SAME_AS links and track how many owners reverse.
  • Mention counts are not event frequencies: use logs like HL7 timestamps, record which field you used, and flag disagreements instead of auto-resolving them.

I don't trust a process diagram I can't trace back to a sentence.

Ask a model for a BPMN diagram and you get something that looks finished and isn't. Feed it an interview or a pile of SOPs and out come clean lanes and tidy boxes. Nobody can tell which boxes came from a real person, which came from a five-year-old SOP, and which the model invented to make the lanes line up.

AI process mapping works when the model extracts and people decide. The LLM turns transcripts, SOPs, tickets and chat into typed graph candidates. Every node and edge points back to the sentence it came from. Nothing counts as true until the person who owns the process says so. Diagrams are views rendered from the graph, and nobody edits them directly.

In The Death of Institutional Memory I wrote about why I'm building Graphite Atlas. This post covers how the pipeline under it works: extract → resolve aliases → owner confirms → event logs add frequency → query and render.

This isn't the clinical-data graph from FHIR Meets Graph Databases. That one models patients, providers and encounters. This one models the work: who does what, in which system, and where it changes hands. The examples are healthcare workflows: referral intake, prior auth and interface ownership. That work crosses roles, systems and organizations, and half of it lives in someone's head.

What AI process mapping should output

Six node types:

  • Step: a unit of work, such as "verify insurance eligibility."
  • Role: who does the work. A role, not a person.
  • System: where the work happens. The EHR, the fax server, the payer portal, the interface engine.
  • Artifact: what moves between steps. A referral packet, an auth number, clinical notes.
  • Handoff: the point where work changes hands.
  • Decision: a branch with criteria.

Make Handoff a node, not an edge. A handoff has its own artifact, its own channel (portal, fax, phone, a shared inbox) and its own way of failing. It's a likely place for a referral to stall. As a bare edge between two roles, it has nowhere to hold any of that. As a node, it gets FROM_ROLE and TO_ROLE edges, and those are what let a renderer draw the lane crossing later.

Here are the schema and the validation gate:

const NODE_TYPES = ['Step','Role','System','Artifact','Handoff','Decision'] as const;
type NodeType = typeof NODE_TYPES[number];
const FLOW: NodeType[] = ['Step','Handoff','Decision'];

// Allowed [from, to] node types per edge. Anything else is rejected.
const EDGES = {
  PERFORMED_BY: [['Step'], ['Role']],
  RUNS_IN:      [['Step'], ['System']],
  PRODUCES:     [['Step'], ['Artifact']],
  REQUIRES:     [['Step'], ['Artifact']],
  NEXT:         [FLOW, FLOW],
  CARRIES:      [['Handoff'], ['Artifact']],
  FROM_ROLE:    [['Handoff'], ['Role']],
  TO_ROLE:      [['Handoff'], ['Role']],
  OWNS:         [['Role'], ['System']],
} satisfies Record<string, [readonly NodeType[], readonly NodeType[]]>;
type EdgeType = keyof typeof EDGES;

type SourceSpan = { docId: string; start: number; end: number; quote: string };
type Status = 'hypothesis' | 'confirmed' | 'rejected';
type Node = { id: string; type: NodeType; name: string; spans: SourceSpan[]; status: Status; confirmedBy?: string };
type Edge = { type: EdgeType; from: string; to: string; spans: SourceSpan[]; status: Status; confirmedBy?: string };

const spansOk = (spans: SourceSpan[], docs: Record<string, string>) =>
  spans.length > 0 && spans.every(s => docs[s.docId]?.slice(s.start, s.end) === s.quote);

// Reject, don't coerce.
function acceptNode(n: Node, docs: Record<string, string>): boolean {
  return (NODE_TYPES as readonly string[]).includes(n.type) && spansOk(n.spans, docs);
}

function acceptEdge(e: Edge, nodes: Map<string, Node>, docs: Record<string, string>): boolean {
  const rule = EDGES[e.type], a = nodes.get(e.from), b = nodes.get(e.to);
  if (!rule || !a || !b) return false;
  if (!rule[0].includes(a.type) || !rule[1].includes(b.type)) return false;
  return spansOk(e.spans, docs);
}

The quote check matters more than it looks. Models paraphrase while claiming to quote. If the quote doesn't match the source slice exactly, the span is fiction and the candidate goes back. Same with types: if the model returns a Department node, reject it. Free-form extraction lets every document invent its own nouns, and you get a graph you can't query. Endpoint rules work the same way. A PERFORMED_BY that points at a System is a type error, not a fact. confirmedBy is always the process owner, never the extractor.

Alias links (SAME_AS) aren't in that table on purpose. They come from merge proposals, covered below.

A worked example: one transcript turn, one SOP line

This is illustrative, with synthetic inputs and no client data. It's the whole pipeline on a tiny input.

Transcript (T1): "I'm the intake coordinator. Referrals come in by fax. I scan them into the EHR, check eligibility on the payer portal, then hand the packet to scheduling."

SOP (S1, §3.2): "The Referral Coordinator verifies eligibility in the payer portal prior to scheduling. Scheduling books the visit in the EHR."

Extraction yields 15 raw candidates:

SourceRoleSystemStepArtifactHandoff
T1intake coordinator, schedulingfax server, EHR, payer portalscan referral, check eligibilityreferral packetintake → scheduling
S1Referral Coordinator, Schedulingpayer portal, EHRverify eligibility, book visitnonenone

Then resolution:

TypeRawMerges proposedClustersFound by nameFound by contextOwner reversals
Role42211not reviewed
System52320not reviewed
Step41301not reviewed
Artifact10100not reviewed
Handoff10100not reviewed
Total1551032

The two merges that matter are the ones names can't find. intake coordinator SAME_AS Referral Coordinator, with the reason: both verify eligibility in the payer portal before the scheduling handoff. "Check eligibility" and "verifies eligibility" merge the same way, as Step aliases. Scheduling stays separate from the coordinator because it performs a different step.

Even case-insensitive exact matching finds only 3 of the 5 merges. That leaves 12 nodes, a phantom second coordinator role, and two eligibility steps that look like rework. Nobody has reviewed anything yet, so the reversal column is empty. Once owners review, that's the column I'd watch.

Two things stay hypotheses. The NEXT edge from eligibility to the handoff has support from both sources, but nobody who owns intake has confirmed it. And "fax server" is an inference: the span says "by fax," and the model named a system. The handoff's channel is unknown too, since T1 says "hand the packet" and nothing about how. That's a question for the owner.

The rendered swimlane has two lanes. Referral Coordinator holds scan and verify. Scheduling holds book visit. The handoff's FROM_ROLE and TO_ROLE edges produce the one lane crossing. Every edge is dashed, because nothing is confirmed yet. That's honest. A clean solid-line diagram from the same input would be lying.

Extracting process steps from transcripts, SOPs and tickets

Each input type is wrong in its own way:

  • SOPs describe the intended process, often as it was years ago.
  • Interviews describe what one person believes happens.
  • Tickets describe what broke.
  • Chat describes the workaround.

You want all four, and you want to know which one said what. That's the job of the span.

Microsoft's GraphRAG paper is the best-known reference pipeline for LLM graph extraction, and it's useful here for what it does and doesn't do:

  • GPT-4 found almost twice as many entity references in 600-token chunks as in 2400-token chunks.
  • It compensates for large chunks with a "gleaning" loop. The extracted entities are fed back, the model is forced to answer yes or no to "did you miss any?", and the loop repeats up to a set limit.
  • It reconciles entity names by exact string matching.
  • Its edge weight is the count of duplicate mentions.

The last two are fine for summarizing a corpus. Both break process mapping. The worked example already showed what exact matching misses. Mention counts come up again with event logs.

Start with small chunks on natural boundaries: one transcript turn, one SOP section, one ticket. The tradeoff is call count. Quartering the chunk size roughly quadruples the calls per document, and each call repeats the schema and instructions. Gleaning instead adds up to your loop limit in extra calls per chunk, each one re-sending the chunk. I take the small chunks, and not for recall alone. A span covering 2400 tokens can't tell an owner which sentence to check. A transcript turn can.

Entity resolution for process maps: merge aliases and count the merges

Process sources are full of aliases. A real interface engine gets called by its product name, "the engine" and a server hostname, often in the same week of tickets. Exact string matching treats those as three systems.

Mistakes in either direction hurt:

  • Under-merge, and the graph shows three systems where there's one. The load is split across aliases, so your single points of failure disappear.
  • Over-merge, and two real roles collapse into one. The handoff between them vanishes, and that handoff was probably the thing you needed to see.

The rules:

  • Block by type. Never merge a Role with a System, however similar the names.
  • Match on context, not just names. Two role names that perform the same steps and touch the same artifacts are strong merge candidates. Two that only look alike are not.
  • The LLM proposes merges with a stated reason. It proposes. It doesn't decide.
  • Store merges as reversible SAME_AS links. Never delete the original candidates.

The proposal record and the numbers to track:

type MergeProposal = {
  type: NodeType;            // blocking: both candidates share it
  keep: string;              // candidate ids; neither is ever deleted
  alias: string;
  reason: string;            // "both verify eligibility in payer portal before scheduling handoff"
  sharedContext: string[];   // ids of steps/artifacts both candidates touch
  status: 'proposed' | 'accepted' | 'reversed';
};

// Clusters = candidates minus accepted merges, assuming each accepted merge joins two different clusters.
function resolutionStats(raw: Node[], merges: MergeProposal[], type: NodeType) {
  const candidates = raw.filter(n => n.type === type).length;
  const reviewed = merges.filter(m => m.type === type && m.status !== 'proposed');
  const accepted = reviewed.filter(m => m.status === 'accepted').length;
  const reversed = reviewed.length - accepted;
  return { candidates, clusters: candidates - accepted, reversed,
           reversalRate: reviewed.length ? reversed / reviewed.length : 0 };
}

Reversal rate is reversed merges over reviewed merges. Proposed merges don't count until an owner has seen them. That number tells you whether resolution can be trusted. If owners keep splitting your merges, your matching is too loose, however good the cluster count looks. It's the same idea as product evals: compare the machine's judgment to a human's, and tune against that.

How to validate an AI-generated process map with process owners

A process owner confirms each edge. Anything unconfirmed stays labeled as a hypothesis everywhere: in queries, in exports, in diagrams.

A graph that mixes guesses with facts gets trusted all at once or distrusted all at once. Both are failures.

The cost is review burden. Take a hypothetical: extraction gives you 400 edges across eight owners. That's 50 each, and nobody reviews a 400-row spreadsheet. So make the review small:

  • Group by owner. The prior auth lead sees only prior auth edges.
  • Show each edge with its quote and three buttons: yes, no, fix.
  • Put conflicting claims side by side so the owner settles them in one pass.
  • Put merges in the same queue. A rejected SAME_AS is how the reversal count gets its data.

If an owner never reviews, the edges stay hypotheses. That's fine. An unconfirmed graph still makes a better interview guide than a blank page. "Is it true eligibility gets checked before the packet goes to scheduling, and how does the packet get there?" gets a better answer than "walk me through your process."

Process mining with event logs: adding HL7 timestamps to the process map

How often something is said is not how often it happens. A duplicate-mention weight tells you what people talk about. A process map needs to know what happens. Where event logs exist, use them for frequencies and durations.

In healthcare, HL7 v2 messages carry timestamps, and you have to pick one. Per the HL7 v2.6 spec:

  • MSH-7 is when the message was created.
  • EVN-2 is when the event was recorded.
  • EVN-3 is when the event was planned.

None of them is guaranteed to be when the thing actually happened. Pick a different field and your durations change. Store the field you used on the edge so the number explains itself.

Parsing differs by version too. These fields are TS in v2.5.1 and DTM in v2.6 and v2.7. Handle both.

When logs and interviews disagree, flag it. Don't auto-resolve in either direction. Say someone tells you every referral gets an eligibility check, and the logs show checks on only some of them. The logs could be right. The person could also be right: logs only see what passes through the system, and a portal lookup, a phone call or a note on someone's monitor is invisible to them. Send the flag to the owner as a question with both pieces of evidence attached.

Process graph queries: single points of failure, handoff chains, retiring a system

The graph earns its keep in queries a diagram can't answer. Prior auth is a good place to start. The AMA reports that practices complete 39 prior authorization requests per physician, per week. If a handoff sits on every one of those requests, each minute it adds is 39 minutes per physician, every week.

// Steps exactly one role can perform
MATCH (s:Step)-[:PERFORMED_BY {status:'confirmed'}]->(r:Role)
WITH s, collect(r) AS roles WHERE size(roles) = 1
RETURN s.name AS step, roles[0].name AS onlyRole;

// Handoff chains from referral received to visit booked
MATCH p = (:Step {name:$from})-[:NEXT*1..20]->(:Step {name:$to})
WHERE all(e IN relationships(p) WHERE e.status = 'confirmed')
WITH [n IN nodes(p) WHERE n:Handoff | n.name] AS handoffs
RETURN handoffs, size(handoffs) AS hops ORDER BY hops DESC LIMIT 5;

// What breaks if we retire a system
MATCH (sys:System {name:$system})<-[:RUNS_IN]-(s:Step)
OPTIONAL MATCH (s)-[:PRODUCES]->(:Artifact)<-[:REQUIRES]-(d:Step)
RETURN s.name AS step, collect(DISTINCT d.name) AS downstream;

// Interfaces nobody owns
MATCH (sys:System) WHERE NOT (:Role)-[:OWNS {status:'confirmed'}]->(sys)
RETURN sys.name;

Long handoff chains are where to look for stalls. The ownership query is the simplest, and often the most useful. When a feed breaks at 2am, a system with no confirmed owner is the finding.

If you're planning for the payer prior auth APIs under CMS-0057-F, map your current chain first. You can't redesign around steps you haven't written down.

Queries filter to confirmed edges by default. Drop the status filter and you see the possible single points of failure. Take that list to the next owner review.

Generate BPMN and swimlane diagrams from the graph

Diagrams are a rendering step:

  • Roles become lanes.
  • Steps become tasks, placed in the lane of their PERFORMED_BY role.
  • Decisions become gateways.
  • Handoffs become lane crossings, from the FROM_ROLE lane to the TO_ROLE lane.
  • Hypothesis edges are drawn dashed, so a diagram never hides how much of it is unconfirmed.

Nobody edits the diagram. A hand-edited diagram starts drifting from reality the day it's drawn. If it's wrong, fix the graph and render again. That's the model I'm building into Graphite Atlas.

Questions people ask

Can AI generate a business process map from interviews and documents?

Yes, if the model extracts and people decide. An LLM can turn transcripts, SOPs, tickets and chat into typed candidates like Steps, Roles, Systems, Artifacts, Handoffs and Decisions. Every claim keeps its source span, and nothing counts as confirmed until a process owner says so.

Should AI process mapping output a BPMN diagram?

Not directly. Build a graph first and render BPMN or swimlane diagrams from it as views: roles become lanes, and handoffs become lane crossings from their FROM_ROLE lane to their TO_ROLE lane. Unconfirmed edges are drawn dashed, and if the diagram is wrong you fix the graph and render again.

Which HL7 timestamp should I use to measure process durations?

MSH-7 is when the message was created, EVN-2 is when the event was recorded, and EVN-3 is when it was planned. None is guaranteed to be when the event actually happened, so pick one deliberately and store which field you used on the edge.

How do you handle entity resolution when extracting process graphs with an LLM?

Block merges by type, have the LLM propose merges with a stated reason based on shared steps and artifacts, and store accepted merges as reversible SAME_AS links. Track raw candidates, clusters and owner-reversed merges per type. A high reversal rate means the matching is too loose.