How to Build a Prior Authorization Packet Assembler with Arcade's Google Docs Toolkit

How to Build a Prior Authorization Packet Assembler with Arcade's Google Docs Toolkit

Arcade.dev Team's avatar
Arcade.dev Team
OCTOBER 21, 2025
8 MIN READ
THOUGHT LEADERSHIP
Rays decoration image
Ghost Icon

Prior authorization processing consumes 30-60 minutes per case in most healthcare practices. Medical staff manually collect patient demographics, clinical notes, lab results, and diagnostic codes from multiple systems, then format everything into submission packets for insurance approval. This guide shows how to build an automated assembler using Arcade's Google Docs toolkit.

The system authenticates healthcare staff, retrieves data from source systems, and compiles structured Google Docs packets ready for submission. Staff authenticate once via OAuth, and Arcade manages token refresh and permissions automatically.

Prerequisites

  • Arcade.dev account with API key
  • Python 3.8+
  • Self-hosted Arcade Engine (Google Docs toolkit requires self-hosting)
  • Google Cloud Console project with OAuth 2.0 credentials
  • Access to EHR/practice management system APIs

Note: The Google Docs toolkit is not available in Arcade Cloud. Deploy a self-hosted Arcade Engine following the deployment guide.

Set Up Arcade Engine for Self-Hosting

Install Dependencies

pip install arcadepy arcade-ai

The arcadepy package provides the Arcade API client. The arcade-ai package includes the Tool Development Kit for custom tool development and the Arcade Engine for self-hosting.

Configure Environment Variables

export ARCADE_API_KEY="your_arcade_api_key"
export GOOGLE_CLIENT_ID="your_google_client_id"
export GOOGLE_CLIENT_SECRET="your_google_client_secret"

Get your API key from the Arcade dashboard. Create Google OAuth credentials in Google Cloud Console.

Configure Arcade Engine

Create an engine.yaml configuration file:

auth:
  providers:
    - id: google-docs-provider
      description: "Google OAuth for Docs access"
      enabled: true
      type: oauth2
      provider_id: google
      client_id: ${env:GOOGLE_CLIENT_ID}
      client_secret: ${env:GOOGLE_CLIENT_SECRET}

api:
  development: false
  host: 0.0.0.0
  port: 9099

tools:
  directors:
    - id: default
      enabled: true
      max_tools: 64

Start the Arcade Engine:

arcade engine start --config engine.yaml

Verify the engine is running at http://localhost:9099/health.

Build the Document Assembler Core

Initialize Arcade Client

import os
from arcadepy import Arcade
from datetime import datetime

class PriorAuthAssembler:
    def __init__(self):
        self.client = Arcade(api_key=os.environ.get("ARCADE_API_KEY"))
        self.authenticated_users = {}

    def get_client(self):
        return self.client

Implement User Authentication

Healthcare applications require per-user authentication. Each staff member authenticates with their own Google account:

async def authenticate_user(self, user_id: str) -> dict:
    """Handle OAuth flow for healthcare staff member"""

    auth_response = await self.client.tools.authorize(
        tool_name="GoogleDocs.CreateBlankDocument",
        user_id=user_id
    )

    if auth_response.status != "completed":
        return {
            "requires_auth": True,
            "auth_url": auth_response.url,
            "message": "Complete Google authorization to access Docs"
        }

    await self.client.auth.wait_for_completion(auth_response)

    self.authenticated_users[user_id] = {
        "authenticated": True,
        "timestamp": datetime.now()
    }

    return {"authenticated": True}

When staff first use the system, they receive an authorization URL. Arcade handles token storage, refresh, and scope management.

Verify Authentication Status

async def ensure_authenticated(self, user_id: str) -> bool:
    """Verify user has valid Google Docs authorization"""

    if user_id not in self.authenticated_users:
        result = await self.authenticate_user(user_id)
        if result.get("requires_auth"):
            raise ValueError(f"User must authorize at: {result['auth_url']}")

    return True

Create Document Assembly Methods

Create Base Document

async def create_document(self, user_id: str, title: str) -> dict:
    """Create new Google Docs document"""

    await self.ensure_authenticated(user_id)

    response = await self.client.tools.execute(
        tool_name="GoogleDocs.CreateBlankDocument",
        input={"title": title},
        user_id=user_id
    )

    return {
        "document_id": response.output.value["document_id"],
        "title": title,
        "url": response.output.value["url"]
    }

The GoogleDocs.CreateBlankDocument tool creates a blank document in the authenticated user's Google Drive.

Append Text Sections

async def append_text(self, user_id: str, document_id: str, text: str) -> dict:
    """Insert text at end of document"""

    response = await self.client.tools.execute(
        tool_name="GoogleDocs.InsertText",
        input={
            "document_id": document_id,
            "text": text
        },
        user_id=user_id
    )

    return response.output.value

The GoogleDocs.InsertText tool appends formatted text to the document end. Each section builds sequentially.

Retrieve Data from Healthcare Systems

Fetch Patient Demographics

async def fetch_patient_data(self, patient_id: str) -> dict:
    """Retrieve patient information from EHR"""

    # Replace with actual EHR API integration
    return {
        "patient_name": "John Doe",
        "dob": "1985-03-15",
        "member_id": "MEM123456789",
        "insurance_provider": "Blue Cross Blue Shield",
        "policy_number": "BCBS-987654321",
        "pcp_name": "Dr. Sarah Johnson",
        "patient_phone": "(555) 123-4567",
        "patient_address": "123 Main St, Anytown, ST 12345"
    }

Fetch Clinical Documentation

async def fetch_clinical_data(self, patient_id: str, encounter_id: str) -> dict:
    """Retrieve clinical documentation from EHR"""

    return {
        "primary_diagnosis": "Chronic Lower Back Pain",
        "icd10_code": "M54.5",
        "secondary_diagnoses": ["Sciatica (M54.4)", "Radiculopathy (M54.1)"],
        "procedure_name": "Lumbar MRI with Contrast",
        "cpt_code": "72148",
        "medical_necessity": "Patient presents with persistent lower back pain radiating to left leg for 8 weeks, unresponsive to conservative treatment.",
        "treatment_history": [
            {
                "name": "Physical Therapy",
                "date": "2025-08-01",
                "outcome": "Minimal improvement after 6 weeks"
            }
        ],
        "physician_notes": "Patient reports 7/10 pain severity. Physical exam reveals positive straight leg raise test. MRI recommended to rule out disc herniation."
    }

Fetch Diagnostic Results

async def fetch_diagnostic_data(self, patient_id: str) -> dict:
    """Retrieve lab and imaging results"""

    return {
        "lab_results": [
            {
                "test_name": "Complete Blood Count",
                "date": "2025-09-10",
                "value": "14.2 g/dL",
                "reference_range": "13.5-17.5 g/dL",
                "status": "Normal"
            }
        ],
        "imaging_studies": [
            {
                "study_type": "Lumbar X-Ray",
                "date": "2025-08-20",
                "facility": "Anytown Radiology Center",
                "findings": "Mild degenerative changes at L4-L5. No acute fracture.",
                "impression": "Age-appropriate degenerative changes"
            }
        ]
    }

Assemble Document Sections

Add Patient Demographics

async def add_demographics_section(
    self,
    user_id: str,
    document_id: str,
    patient_data: dict
) -> None:
    """Add patient demographics to document"""

    text = f"""PATIENT DEMOGRAPHICS
====================

Patient Name: {patient_data['patient_name']}
Date of Birth: {patient_data['dob']}
Member ID: {patient_data['member_id']}
Insurance Provider: {patient_data['insurance_provider']}
Policy Number: {patient_data['policy_number']}
Primary Care Physician: {patient_data['pcp_name']}
Phone: {patient_data['patient_phone']}
Address: {patient_data['patient_address']}

"""

    await self.append_text(user_id, document_id, text)

Add Clinical Documentation

async def add_clinical_section(
    self,
    user_id: str,
    document_id: str,
    clinical_data: dict
) -> None:
    """Add clinical documentation to document"""

    treatment_list = '\n'.join([
        f"- {t['name']} ({t['date']}): {t['outcome']}"
        for t in clinical_data['treatment_history']
    ])

    text = f"""CLINICAL DOCUMENTATION
======================

Primary Diagnosis: {clinical_data['primary_diagnosis']} (ICD-10: {clinical_data['icd10_code']})
Secondary Diagnoses: {', '.join(clinical_data['secondary_diagnoses'])}

Procedure Requested: {clinical_data['procedure_name']} (CPT: {clinical_data['cpt_code']})
Medical Necessity: {clinical_data['medical_necessity']}

Previous Treatments:
{treatment_list}

Clinical Notes:
{clinical_data['physician_notes']}

"""

    await self.append_text(user_id, document_id, text)

Add Diagnostic Results

async def add_diagnostics_section(
    self,
    user_id: str,
    document_id: str,
    diagnostics: dict
) -> None:
    """Add lab and imaging results to document"""

    text = "DIAGNOSTIC RESULTS\n==================\n\nLab Results:\n"

    for lab in diagnostics['lab_results']:
        text += f"""
{lab['test_name']} ({lab['date']}):
  Result: {lab['value']}
  Reference: {lab['reference_range']}
  Status: {lab['status']}
"""

    text += "\nImaging Studies:\n"

    for imaging in diagnostics['imaging_studies']:
        text += f"""
{imaging['study_type']} ({imaging['date']}):
  Facility: {imaging['facility']}
  Findings: {imaging['findings']}
  Impression: {imaging['impression']}
"""

    await self.append_text(user_id, document_id, text)

Add Submission Metadata

async def add_metadata_section(self, user_id: str, document_id: str) -> None:
    """Add document generation metadata"""

    text = f"""

SUBMISSION INFORMATION
======================

Document Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
Generated By: {user_id}
System: Prior Authorization Automated Assembler

Attestation: Information contained in this request is accurate and complete.

"""

    await self.append_text(user_id, document_id, text)

Orchestrate Complete Assembly

async def assemble_packet(
    self,
    user_id: str,
    patient_id: str,
    encounter_id: str
) -> dict:
    """Execute complete prior auth packet assembly"""

    try:
        await self.ensure_authenticated(user_id)

        # Retrieve all data
        patient_data = await self.fetch_patient_data(patient_id)
        clinical_data = await self.fetch_clinical_data(patient_id, encounter_id)
        diagnostics = await self.fetch_diagnostic_data(patient_id)

        # Create document
        doc_title = f"Prior Auth - {patient_data['patient_name']} - {clinical_data['cpt_code']}"
        document = await self.create_document(user_id, doc_title)

        # Assemble sections
        await self.add_demographics_section(user_id, document["document_id"], patient_data)
        await self.add_clinical_section(user_id, document["document_id"], clinical_data)
        await self.add_diagnostics_section(user_id, document["document_id"], diagnostics)
        await self.add_metadata_section(user_id, document["document_id"])

        return {
            "success": True,
            "document_id": document["document_id"],
            "document_url": document["url"],
            "title": doc_title,
            "timestamp": datetime.now().isoformat()
        }

    except Exception as e:
        return {
            "success": False,
            "error": str(e)
        }

Handle Multiple Users

Healthcare organizations have multiple staff members. Arcade maintains separate credentials for each user:

class MultiUserAssembler:
    def __init__(self):
        self.assembler = PriorAuthAssembler()
        self.sessions = {}

    async def process_request(
        self,
        staff_email: str,
        patient_id: str,
        encounter_id: str
    ) -> dict:
        """Process request with staff-specific context"""

        result = await self.assembler.assemble_packet(
            user_id=staff_email,
            patient_id=patient_id,
            encounter_id=encounter_id
        )

        if result["success"]:
            self.sessions[staff_email] = {
                "last_document": result["document_id"],
                "last_assembly": result["timestamp"]
            }

        return result

Build Error Handling

class AssemblyError(Exception):
    """Base exception for assembly errors"""
    pass

class AuthenticationError(AssemblyError):
    def __init__(self, message: str, auth_url: str):
        self.message = message
        self.auth_url = auth_url
        super().__init__(message)

async def handle_error(error: Exception) -> dict:
    """Handle assembly errors"""

    if isinstance(error, AuthenticationError):
        return {
            "error_type": "authentication",
            "requires_action": True,
            "auth_url": error.auth_url
        }

    return {
        "error_type": "system",
        "requires_action": False,
        "message": str(error)
    }

Implement Retry Logic

import asyncio

async def execute_with_retry(func, max_retries: int = 3, *args, **kwargs):
    """Execute function with exponential backoff"""

    for attempt in range(max_retries):
        try:
            return await func(*args, **kwargs)
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            delay = 2 ** attempt
            await asyncio.sleep(delay)

Build FastAPI Web Service

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()
multi_user_system = MultiUserAssembler()

class AssemblyRequest(BaseModel):
    patient_id: str
    encounter_id: str
    staff_email: str

class AssemblyResponse(BaseModel):
    status: str
    document_id: str | None = None
    document_url: str | None = None
    message: str | None = None

@app.post("/api/assemble", response_model=AssemblyResponse)
async def assemble_packet(request: AssemblyRequest):
    """Trigger prior auth packet assembly"""

    result = await multi_user_system.process_request(
        staff_email=request.staff_email,
        patient_id=request.patient_id,
        encounter_id=request.encounter_id
    )

    if not result["success"]:
        raise HTTPException(status_code=500, detail=result.get("error"))

    return AssemblyResponse(
        status="completed",
        document_id=result["document_id"],
        document_url=result["document_url"],
        message="Prior authorization packet assembled"
    )

@app.get("/api/status/{staff_email}")
async def check_status(staff_email: str):
    """Check authentication status"""

    session = multi_user_system.sessions.get(staff_email)

    return {
        "authenticated": session is not None,
        "last_assembly": session.get("last_assembly") if session else None
    }

Optimize Performance

Parallel Data Retrieval

import asyncio

async def fetch_all_data(self, patient_id: str, encounter_id: str) -> tuple:
    """Fetch data from multiple sources concurrently"""

    results = await asyncio.gather(
        self.fetch_patient_data(patient_id),
        self.fetch_clinical_data(patient_id, encounter_id),
        self.fetch_diagnostic_data(patient_id)
    )

    return results

Parallel fetching reduces assembly time by 40-60% when source systems have network latency.

Cache User Tools

from collections import OrderedDict
import time

class ToolCache:
    def __init__(self, max_size: int = 100, ttl: int = 3600):
        self.cache = OrderedDict()
        self.max_size = max_size
        self.ttl = ttl

    def set(self, user_id: str, tools: list):
        if user_id in self.cache:
            self.cache.move_to_end(user_id)

        self.cache[user_id] = {
            "tools": tools,
            "timestamp": time.time()
        }

        if len(self.cache) > self.max_size:
            self.cache.popitem(last=False)

    def get(self, user_id: str):
        entry = self.cache.get(user_id)
        if not entry:
            return None

        if time.time() - entry["timestamp"] > self.ttl:
            del self.cache[user_id]
            return None

        self.cache.move_to_end(user_id)
        return entry["tools"]

Track Performance Metrics

class PerformanceMonitor:
    def __init__(self):
        self.metrics = {
            "completed": 0,
            "failed": 0,
            "avg_time": 0.0
        }
        self.times = []

    async def track(self, user_id: str, duration: float, success: bool):
        """Track assembly performance"""

        self.times.append(duration)

        if success:
            self.metrics["completed"] += 1
        else:
            self.metrics["failed"] += 1

        self.metrics["avg_time"] = sum(self.times) / len(self.times)

    def report(self) -> dict:
        """Generate health report"""

        total = self.metrics["completed"] + self.metrics["failed"]
        success_rate = (self.metrics["completed"] / total * 100) if total > 0 else 0

        return {
            "status": "healthy" if success_rate > 95 else "degraded",
            "total": total,
            "success_rate": f"{success_rate:.2f}%",
            "avg_time": f"{self.metrics['avg_time']:.2f}s"
        }

Extend with Additional Toolkits

Email Submission via Gmail

Integrate the Gmail toolkit to send completed packets:

async def submit_via_email(
    self,
    user_id: str,
    document_url: str,
    insurer_email: str,
    patient_name: str
):
    """Email packet to insurance provider"""

    await self.client.tools.execute(
        tool_name="Gmail.SendEmail",
        input={
            "to": insurer_email,
            "subject": f"Prior Authorization Request - {patient_name}",
            "body": f"Prior authorization packet: {document_url}"
        },
        user_id=user_id
    )

Schedule Follow-ups

Use the Google Calendar toolkit for reminders:

async def schedule_followup(
    self,
    user_id: str,
    patient_name: str,
    days: int = 3
):
    """Create calendar reminder"""

    from datetime import timedelta
    followup_date = datetime.now() + timedelta(days=days)

    await self.client.tools.execute(
        tool_name="GoogleCalendar.CreateEvent",
        input={
            "summary": f"Follow up - {patient_name} prior auth",
            "start_time": followup_date.isoformat(),
            "duration_minutes": 15
        },
        user_id=user_id
    )

Team Notifications

Integrate the Slack toolkit for notifications:

async def notify_team(
    self,
    document_url: str,
    patient_name: str,
    channel: str = "#prior-auth"
):
    """Send Slack notification"""

    await self.client.tools.execute(
        tool_name="Slack.SendMessage",
        input={
            "channel": channel,
            "text": f"Prior auth completed for {patient_name}: {document_url}"
        },
        user_id="system_user"
    )

Production Deployment

HIPAA Compliance

  • Configure Business Associate Agreement with Arcade
  • Use encrypted connections for all API calls
  • Implement audit logging for all document access
  • Restrict system access to authorized personnel

Scale Architecture

Deploy with worker processes for concurrent assembly:

from celery import Celery

app = Celery('prior_auth', broker='redis://localhost:6379')

@app.task
async def assemble_task(user_id: str, patient_id: str, encounter_id: str):
    """Background assembly task"""
    assembler = PriorAuthAssembler()
    return await assembler.assemble_packet(user_id, patient_id, encounter_id)

Monitor System Health

@app.get("/health")
async def health_check():
    """System health endpoint"""

    monitor = PerformanceMonitor()
    report = monitor.report()

    return {
        "status": report["status"],
        "metrics": report
    }

Results

Automated prior authorization packet assembly delivers:

  • Time reduction: 30-60 minutes manual work to under 2 minutes automated
  • Accuracy: Direct EHR data extraction eliminates transcription errors
  • Compliance: OAuth authentication maintains proper access controls
  • Scalability: Multiple staff members work concurrently with independent credentials
  • Extensibility: Additional toolkits enable email, calendar, and notifications

The system handles OAuth authentication that typically blocks healthcare AI implementations, letting developers focus on clinical workflows.

Resources

SHARE THIS POST

RECENT ARTICLES

Rays decoration image
THOUGHT LEADERSHIP

Agent Skills vs Tools: What Actually Matters

The agent ecosystem has a terminology problem that masks a real architectural choice. "Tools" and "skills" get used interchangeably in marketing decks and conference talks, but they represent fundamentally different approaches to extending agent capabilities. Understanding this distinction is the difference between building agents that work in demos versus agents that work in production. But here's the uncomfortable truth that gets lost in the semantic debates: from the agent's perspective, it'

Rays decoration image
THOUGHT LEADERSHIP

Using LangChain and Arcade.dev to Build AI Agents For Consumer Packaged Goods: Top 3 Use Cases

Key Takeaways * CPG companies hit a multi-user authorization wall, not a capability gap: Most agent projects stall in production because leaders can’t safely govern what permissions and scopes an agent has after it’s logged in across fragmented, domain specific systems (ERPs, retailer portals, communications). Arcade.dev’s MCP runtime replaces months of custom permissioning, token/secret handling, and auditability work. * Weather-based demand forecasting delivers fastest ROI: Unilever achiev

Rays decoration image
THOUGHT LEADERSHIP

Using LangChain and Arcade.dev to Build AI Agents For Energy & Utilities: Top 3 Use Cases

Key Takeaways * Multi-user authorization blocks AI agent production in energy utilities: While AI agents show transformative potential across industries, energy utilities struggle to move past proof-of-concept because agents need secure, scoped access to SCADA systems, customer databases, and field operations platforms — Arcade.dev's MCP runtime solves this gap * LangChain + LangGraph are widely used for agent orchestration: Together they provide a proven way to model multi-step utility work

Blog CTA Icon

Get early access to Arcade, and start building now.