case study · Rivio · 2026
API, RPA, agent, human: a durable integration chain for hospital billing
Hospitals and health insurers exchange about a thousand operations a day: eligibility checks, authorizations, billing. Every insurer offers a different mix of ways in. I designed and built the orchestration that tries each way in order, and hands the case to a person only when it can't prove the job is done.
- Role
- Senior Software Engineer
- Scale
- ~1,000 operations a day across 5 insurers
- Outcome
- ~90% resolved without a person
- Stack
- Temporal Cloud · Python · Google Cloud Pub/Sub · GCP · MongoDB
The problem
Rivio manages the revenue cycle for hospitals. A large part of that cycle is talking to health insurers: checking whether a patient is covered, requesting authorizations, submitting billing guides. We call each of these an integration.
When an integration failed, someone on the operations team opened the insurer’s portal and did it by hand. At around a thousand operations a day, every failed integration added to that pile.
The hard part wasn’t any single insurer. It was the mix. Some insurers have an API. Others only have a portal. Some have both, but only for certain operations. Eligibility at Insurer A could go through every channel. Eligibility at Insurer B only worked through the portal. So the variation wasn’t per insurer, it was per operation.
The integrations already ran as event-driven services on Pub/Sub. What was missing was something that owned the whole life of a request: which channels it had tried, what happened in each, and what to do next.
What the system had to guarantee
I wrote down four rules before writing code:
- No request gets lost. Every operation ends in a known state, even if a worker crashes halfway through.
- Nothing that changes state at an insurer runs twice. A duplicate billing submission is worse than a slow one.
- Every failure ends somewhere a person can see it. No silent drops, no dead letters nobody reads.
- A new integration never touches the orchestrator. It still needs its own code in the tier that talks to the insurer, like an API client or an RPA script. The chain itself only reads config.
The chain
Every operation runs through the same four tiers, always in the same order:
- API. The insurer’s own API, when it has one. Fast and cheap.
- RPA. A scripted run through the insurer’s portal, built on Playwright.
- Agent. An LLM-driven browser agent that operates the portal from a written description of the task.
- Ops. A task for the operations team on our internal operations platform.
Each tier is a separate service. An orchestrator workflow in Temporal decides which tier to call, waits for the result, and moves down the chain when a tier fails.
↩ The agent's run is saved as a Python script. A developer reviews it, and next time it runs as RPA.
- 00:00.000WorkflowStartedauthorization.insurer_c · req 48215
- 00:00.010TierStartedapi
- 00:01.204ActivityFailedapi.request_authorization · 503 · attempt 1/3
- 00:03.412ActivityFailedapi.request_authorization · 503 · attempt 2/3
- 00:07.905ActivityFailedapi.request_authorization · 503 · attempt 3/3
- 00:07.911TierStartedrpa
- 00:08.120ActivityFailedrpa.request_authorization · no script for this flow
- 00:08.126TierStartedagent
- 00:08.340ActivityStartedagent.run · prompt: authorization / insurer_c · browser: panda
- 02:31.077ActivityHeartbeatagent.run · step 7/14 · filling procedure codes
- 04:20.512ActivityCompletedagent.run · authorization submitted
- 04:20.530ScriptSavedgenerated script → mongodb · review job queued
- 04:20.538WorkflowCompletedresolved by agent
Why Temporal
Pub/Sub is good at moving messages. It doesn’t remember where a request is. With a chain of up to four attempts, some of which take minutes, that memory is the whole problem.
Temporal gave us that for free:
- Durable state. If a worker dies in the middle of an RPA run, the workflow picks up where it stopped instead of starting over or disappearing.
- Retries and timeouts as configuration. Each activity declares its own policy instead of every service writing its own retry loop.
- Signals. A workflow can wait for hours for a person to resolve a case, then continue as if nothing happened.
- History. Every workflow keeps a full event history, so “what happened to this request?” always has an answer.
We ran on Temporal Cloud, with Python workers on GCP. Temporal task queues route work to each tier’s workers, and Pub/Sub still connects the parts of the platform that were already event-driven.
One config per operation, not per insurer
The order of the tiers never changes. Each operation only declares which tiers it may use and how carefully to retry:
# illustrative
eligibility.insurer_b:
tiers: [rpa, agent, human] # portal only
retry: read_only
eligibility.insurer_a:
tiers: [api, rpa, agent, human]
retry: read_only
billing.submit_guide:
tiers: [api, rpa, agent, human]
retry: state_changing
This kept the orchestrator small. It doesn’t know anything about Insurer A or Insurer B. It reads the config and walks the chain.
Portal credentials follow the same idea. Every integration uses one shared activity that fetches credentials from our secret manager at run time. Workers and prompts never hold them.
Retries depend on side effects
Checking eligibility twice is harmless. Submitting a billing guide twice is not.
So retry policies follow what an operation does, not which tier runs it. Read-only operations retry freely. Operations that create state at the insurer get conservative policies, with few attempts and no retry after a timeout that might have gone through.
The interesting case is a run that ends in an unknown state: the RPA clicked “submit”, then timed out before it could read the confirmation. Did the guide go through? The system can’t know. Retrying could submit it twice.
If the orchestrator can’t prove an operation finished, it hands it to a person instead of trying again.
The workflow stops, creates a task on the operations platform, and waits for a signal. Someone checks the portal, confirms what happened, and the workflow finishes with the right result. It takes longer, but it never creates a duplicate.
The slow tier teaches the fast one
The agent tier is the part people ask about.
Each operation has a prompt template, written in plain language: what to open, what to fill in, what counts as success. The agent drives a real browser through the insurer’s portal. We had two options: a regular Chrome session, or Panda, a browser built for agents.
The agent is the slowest tier, taking between 1 and 7 minutes depending on the operation, which is why it comes last. That’s fine, because every integration is an asynchronous job. Nobody sits in front of a loading screen waiting for it.
What made it worth building was the feedback loop. The first time the agent succeeds on a new flow, the run is saved as a Python script in MongoDB. An async job picks it up and opens a pull request against the RPA service. A developer tests it and merges it. From then on, that flow runs as RPA: seconds instead of minutes, and no model in the loop.
The agent covered flows we didn’t have scripts for yet, and every success made the agent tier less needed for that flow.
The human in the loop
The last tier isn’t a failure mode. It’s part of the design.
When a case reaches ops, it shows up as a task on the internal operations platform with the context the workflow collected: which tiers ran, what each one returned, and why the chain stopped. I also built backoffice APIs (FastAPI and Node.js/TypeScript) for handling these exceptions and manual interventions. When the operator resolves the task, the platform sends a signal back to the waiting workflow, which closes the request with the correct state.
Seeing the whole path
A single integration can touch the orchestrator, the API service, the RPA service, the agent and the ops platform. When something goes wrong, the question is never “which service failed?” but “what happened to this request?”
Every service was monitored in Datadog, and logs were correlated across services. You could follow one integration from the moment it entered the chain to the tier that resolved it, through every service in between, instead of reading five separate log streams and guessing how they line up.
Keeping patient data out of plain text
These workflows carry health and billing data, and Temporal Cloud stores every workflow’s inputs and results in its history. We didn’t want any of that readable outside our infrastructure.
Workers encrypt every payload before it leaves our side, so Temporal Cloud only ever stores ciphertext. A codec server, running in our own environment, decrypts payloads for people with access when they inspect a workflow in the Temporal UI. The convenience of the event history stays, and the sensitive data stays with us.
Results
- About 1,000 operations a day across 5 insurers run through the chain.
- ~50% resolve at the API tier.
- ~40% resolve through RPA or the agent.
- ~10% reach a person, and those are the cases that need one.
- New integrations took about half the time, at least. We didn’t measure it precisely, but describing a flow in plain language and letting the agent run it (and write the first RPA script) replaced most of the work of building each integration by hand.
What I’d do next
Guardrails for the agent. The agent’s scope was bounded by prompt templates we controlled, and that was enough in practice. It isn’t a long-term answer, especially since the agent also ran operations that change state at insurers. Next I’d add an allowlist of actions per operation, require a confirmation step before anything that changes state, and redact patient data from agent logs. In healthcare, LGPD makes that last one non-negotiable.
Cost and latency per tier. We knew the agent was slow. I’d track cost and duration per tier and per operation, so the decision to write an RPA script is driven by numbers instead of instinct.
Idempotency at the source. Handing unknown states to a person is safe, but it’s still manual work. Where an insurer offers a way to look up a submission, the workflow could check before deciding, and send fewer cases to ops.
Stack: Temporal Cloud · Python (Pydantic, MyPy, Pytest) · Google Cloud Pub/Sub · GCP · MongoDB · Playwright · LLM browser agents · Datadog · FastAPI · Node.js/TypeScript