HealthcareParse

Doctor's Notes Parser

Extracts patient information and medication details from medical prescriptions.

Ship it with Extend

Live pipeline

a real document, processed end to end · view only
Source documentdoctor's notes example.jpeg

Step-by-step

A medical prescription is a written order issued by a licensed physician that specifies medications, dosages, and administration instructions for a patient, along with the prescriber's credentials for pharmacy fulfillment. This template takes in Medical Prescription and outputs JSON (.json) with structured patient demographics, medication details, dosage instructions, and physician credentials extracted from the prescription document by using Extend's Parse primitives.

Input
Medical Prescription
Compatible document types (full list)
.pdf.docx.xlsx.png.jpg.jpeg.tiff.tif.svg.heic.heif.bmp.gif.webp.psd.xls.xltm.xltx.ods.doc.wpd.dotx.odt.pptx.ppt.ppm.csv.txt.html.xml.rtf.lis.md.eml.pcx
Step 1

Parse

Converts the document into clean, layout-aware markdown plus structured blocks with spatial metadata.

InputSource document — PDF, image, spreadsheet, presentation, or scan
Config
advancedOptions.agenticOcrEnabledfalsechanged
advancedOptions.enrichmentFormat"xml"changed
advancedOptions.excelIncludeCellFormattingfalsechanged
advancedOptions.excelIncludeCellMetadatafalsechanged
advancedOptions.excelParsingMode"advanced"changed
advancedOptions.excelSkipCalculationfalsechanged
advancedOptions.excelSkipHiddenContentfalsechanged
advancedOptions.excelUseRawCellValuesfalsechanged
advancedOptions.pageBreaksEnabledfalsechanged
advancedOptions.pageRotationEnabledtruechanged
advancedOptions.verticalGroupingThreshold1changed
blockOptions.barcodes.imageClippingEnabledfalsechanged
blockOptions.barcodes.readingEnabledfalsechanged
blockOptions.figures.advancedChartExtractionEnabledfalsechanged
blockOptions.figures.customInstructions""changed
blockOptions.figures.enabledtruechanged
blockOptions.figures.figureImageClippingEnabledtruechanged
blockOptions.formulas.enabledfalsechanged
blockOptions.keyValue.blankFieldFormattingEnabledfalsechanged
blockOptions.tables.agentic.enabledfalsechanged
blockOptions.tables.cellBlocksEnabledfalsechanged
blockOptions.tables.tableHeaderContinuationEnabledfalsechanged
blockOptions.tables.targetFormat"html"changed
blockOptions.text.agentic.enabledtruechanged
blockOptions.text.signatureDetectionEnabledfalsechanged
chunkingStrategy.options.maxCharacters10000changed
chunkingStrategy.options.minCharacters500changed
chunkingStrategy.type"page"changed
engineVersion"2.0.0"changed
target"markdown"changed
engine"parse_performance"
OutputMarkdown chunked by page or section, plus typed blocks (text, table, figure) with bounding boxes

You can learn more about Parse configuration in Extend's Parse documentation.

Example code

{
  "name": "Medical Prescriptions Rx Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              },
              "signatureDetectionEnabled": false
            },
            "tables": {
              "agentic": {
                "enabled": false
              },
              "targetFormat": "html",
              "cellBlocksEnabled": false,
              "tableHeaderContinuationEnabled": false
            },
            "figures": {
              "enabled": true,
              "customInstructions": "",
              "figureImageClippingEnabled": true,
              "advancedChartExtractionEnabled": false
            },
            "barcodes": {
              "readingEnabled": false,
              "imageClippingEnabled": false
            },
            "formulas": {
              "enabled": false
            },
            "keyValue": {
              "blankFieldFormattingEnabled": false
            }
          },
          "chunkingStrategy": {
            "type": "page",
            "options": {
              "maxCharacters": 10000,
              "minCharacters": 500
            }
          }
        }
      }
    }
  ]
}
# Medical Prescriptions Rx Processing — Extend AI Skill

## What this pipeline does

Converts handwritten and printed medical prescriptions into structured markdown using optical character recognition and agentic parsing. Captures patient demographics, medication details, dosage instructions, physician credentials, and signatures—preparing prescription data for pharmacy fulfillment systems and patient records.

## When to use this

- **Pharmacy intake systems**: Digitize paper prescriptions before entering into PMS software
- **Electronic health records (EHR) ingestion**: Convert scanned Rx documents into machine-readable text for clinical systems
- **Insurance claim processing**: Extract prescription metadata for coverage verification and billing
- **Prescription audit trails**: Create searchable markdown records of all received prescriptions for compliance
- **Handwriting-heavy workflows**: When prescriptions contain physician handwriting, signatures, and annotations that require intelligent OCR

## Processor pipeline

### Step 1: Parse (parse_performance engine, agentic OCR enabled)

**Processor**: `parseRuns.createAndPoll()`  
**Purpose**: Convert the prescription image/PDF into clean markdown, preserving structure, signatures, and legibility.

**Key config choices**:
- **Engine**: `parse_performance` — balances speed and accuracy for medical documents
- **Agentic OCR**: enabled for text blocks — handles physician handwriting and variable layouts
- **Figure extraction**: enabled with image clipping — captures signature blocks, stamps, logos
- **Tables**: disabled — prescriptions rarely contain tabular data requiring HTML format
- **Barcodes**: disabled — barcode reading not required for initial parsing
- **Chunking**: page-level with 500–10,000 character bounds — each prescription is typically 1–2 pages

**Why this config**: Medical prescriptions have variable layouts (preprinted forms + handwriting). Agentic OCR excels at interpreting context-dependent fields (e.g., "Qty:" next to a hand-scrawled number). Page-level chunking keeps each prescription intact rather than fragmenting mid-document.

## TypeScript implementation

```typescript
import { ExtendClient } from "extend-ai";
import fs from "fs";

const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });

/**
 * Process a medical prescription document:
 * 1. Upload the prescription file
 * 2. Parse to markdown using agentic OCR
 * 3. Return structured markdown for downstream processing
 */
export async function processMedicalPrescriptionsRx(filePath: string) {
  // Convert local file to data URL for SDK compatibility
  const fileBuffer = fs.readFileSync(filePath);
  const dataUrl = `data:application/octet-stream;base64,${fileBuffer.toString("base64")}`;

  console.log(`[1/2] Uploading prescription: ${filePath}`);

  // Upload the prescription file
  const uploaded = await client.files.upload({
    file: { url: dataUrl },
  });
  const fileId = uploaded.id;
  console.log(`✓ File uploaded: ${fileId}`);

  console.log(`[2/2] Parsing prescription to markdown with agentic OCR...`);

  // Parse with agentic OCR enabled for handwriting
  const parseRun = await client.parseRuns.createAndPoll({
    file: { id: fileId },
    config: {
      blockOptions: {
        figures: {
          enabled: true,
          figureImageClippingEnabled: true,
          advancedChartExtractionEnabled: false,
        },
        text: {
          // Agentic OCR handles physician handwriting and variable field placement
          agentic: {
            enabled: true,
          },
        },
        tables: {
          targetFormat: "html",
          agentic: {
            enabled: false, // Prescriptions don't use complex tables
          },
        },
        barcodes: {
          readingEnabled: false,
        },
      },
      chunkingStrategy: {
        type: "page",
        options: {
          minCharacters: 500,
          maxCharacters: 10000,
        },
      },
    },
  });

  if (parseRun.status !== "PROCESSED") {
    throw new Error(
      `Parse failed with status: ${parseRun.status}. Message: ${parseRun.errorMessage}`
    );
  }

  // Concatenate all chunks into single markdown document
  const markdown = parseRun.output.chunks.map((chunk) => chunk.content).join("\n\n");

  console.log(`✓ Prescription parsed successfully`);
  console.log(`\n--- PARSED PRESCRIPTION (MARKDOWN) ---\n`);
  console.log(markdown);
  console.log(`\n--- END PRESCRIPTION ---\n`);

  return {
    fileId,
    markdown,
    chunkCount: parseRun.output.chunks.length,
  };
}

// For direct execution: node script.ts <file-path>
if (require.main === module) {
  const filePath = process.argv[2];
  if (!filePath) {
    console.error("Usage: npx ts-node script.ts <prescription-file-path>");
    process.exit(1);
  }
  processMedicalPrescriptionsRx(filePath)
    .then((result) => {
      console.log(`\n✓ Pipeline complete. Parsed ${result.chunkCount} chunk(s).`);
    })
    .catch((err) => {
      console.error(`✗ Error:`, err.message);
      process.exit(1);
    });
}
```

## CLI equivalent

```bash
# Upload and parse prescription to markdown
extend parse my_prescription.pdf \
  --engine parse_performance \
  --agentic-text \
  --figure-clipping \
  --chunk-strategy page \
  --min-chars 500 \
  --max-chars 10000

# Output: prescription.md with full markdown content
```

## Accuracy tips

1. **Use agentic OCR for text blocks** — prescriptions often contain physician cursive, variable field locations, and abbreviations (e.g., "QID", "PRN"). Agentic parsing interprets context.

2. **Enable figure clipping** — signature blocks, physician stamps, and DEA registration numbers are often image-based; clipping preserves them for validation.

3. **Set realistic character bounds** — 500–10,000 characters covers most single-page Rx forms; adjust if you receive multi-page prescriptions or single-medication slips.

4. **Disable unnecessary blocks** — barcodes, advanced formulas, and complex tables add processing time with zero benefit for prescriptions. Keep parsing focused.

5. **Post-parse regex validation** — after markdown generation, use regex to validate key fields:
   - Patient name: `(?i)(patient|name):\s*(.+?)(?:\n|$)`
   - Drug name: `(?i)(medication|drug):\s*(.+?)(?:\n|$)`
   - Physician signature/license: `(?i)(dr\.|md|dea|license):\s*(.+?)(?:\n|$)`

6. **Test on handwritten samples early** — agentic OCR quality varies by handwriting legibility; validate on representative samples before bulk processing.

## Trade-offs & alternatives

| Choice | Trade-off | When to use | Alternative |
|--------|-----------|------------|-------------|
| **Agentic OCR enabled** | +accuracy, +latency | Default for all Rx (handwriting is common) | Disable for pre-printed forms only; saves ~20% latency |
| **Figure clipping enabled** | +file size, +latency | Capture signatures & stamps | Disable if you only need text; saves ~10% latency |
| **Page-level chunking** | Keeps each Rx intact | All prescriptions (typical: 1–2 pages) | Use document-level chunking if bulk-processing multi-Rx batches |
| **Parse over Extract** | Markdown (unstructured) output | When Rx formats vary widely or you need full text for review | Use Extract + Zod schema if all prescriptions follow a consistent form template |

**When to add human review**: 
- Confidence score < 0.85 (if available from downstream classification)
- Ambiguous dosage instructions (e.g., handwritten dose that OCR flags as uncertain)
- Missing critical fields (patient ID, drug name, physician name)

**Sync vs. async**: This pipeline uses `createAndPoll()`, which is synchronous and waits for completion. For batch processing 100+ prescriptions, consider building an async queue (see Extend workflows feature).

---
import { ExtendClient } from "extend-ai";
import fs from "fs";

const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });

/**
 * Process a medical prescription document:
 * 1. Upload the prescription file
 * 2. Parse to markdown using agentic OCR
 * 3. Return structured markdown for downstream processing
 */
export async function processMedicalPrescriptionsRx(filePath: string) {
  // Convert local file to data URL for SDK compatibility
  const fileBuffer = fs.readFileSync(filePath);
  const dataUrl = `data:application/octet-stream;base64,${fileBuffer.toString("base64")}`;

  console.log(`[1/2] Uploading prescription: ${filePath}`);

  // Upload the prescription file
  const uploaded = await client.files.upload({
    file: { url: dataUrl },
  });
  const fileId = uploaded.id;
  console.log(`✓ File uploaded: ${fileId}`);

  console.log(`[2/2] Parsing prescription to markdown with agentic OCR...`);

  // Parse with agentic OCR enabled for handwriting
  const parseRun = await client.parseRuns.createAndPoll({
    file: { id: fileId },
    config: {
      blockOptions: {
        figures: {
          enabled: true,
          figureImageClippingEnabled: true,
          advancedChartExtractionEnabled: false,
        },
        text: {
          // Agentic OCR handles physician handwriting and variable field placement
          agentic: {
            enabled: true,
          },
        },
        tables: {
          targetFormat: "html",
          agentic: {
            enabled: false, // Prescriptions don't use complex tables
          },
        },
        barcodes: {
          readingEnabled: false,
        },
      },
      chunkingStrategy: {
        type: "page",
        options: {
          minCharacters: 500,
          maxCharacters: 10000,
        },
      },
    },
  });

  if (parseRun.status !== "PROCESSED") {
    throw new Error(
      `Parse failed with status: ${parseRun.status}. Message: ${parseRun.errorMessage}`
    );
  }

  // Concatenate all chunks into single markdown document
  const markdown = parseRun.output.chunks.map((chunk) => chunk.content).join("\n\n");

  console.log(`✓ Prescription parsed successfully`);
  console.log(`\n--- PARSED PRESCRIPTION (MARKDOWN) ---\n`);
  console.log(markdown);
  console.log(`\n--- END PRESCRIPTION ---\n`);

  return {
    fileId,
    markdown,
    chunkCount: parseRun.output.chunks.length,
  };
}

// For direct execution: node script.ts <file-path>
const isDirectRun =
  typeof process !== "undefined" &&
  process.argv[1] &&
  require.main === module;

if (isDirectRun) {
  const filePath = process.argv[2];
  if (!filePath) {
    console.error("Usage: npx ts-node script.ts <prescription-file-path>");
    process.exit(1);
  }
  processMedicalPrescriptionsRx(filePath)
    .then((result) => {
      console.log(`\n✓ Pipeline complete. Parsed ${result.chunkCount} chunk(s).`);
    })
    .catch((err) => {
      console.error(`✗ Error:`, err.message);
      process.exit(1);
    });
}
import os
import sys
import base64
from extend_ai import Extend


def process_medical_prescriptions_rx(file_path: str) -> dict:
    """
    Process a medical prescription document:
    1. Upload the prescription file
    2. Parse to markdown using agentic OCR
    3. Return structured markdown for downstream processing
    """
    client = Extend(token=os.environ["EXTEND_API_KEY"])

    # Read file and convert to base64 data URL
    with open(file_path, "rb") as f:
        file_buffer = f.read()
    data_url = f"data:application/octet-stream;base64,{base64.b64encode(file_buffer).decode('utf-8')}"

    print(f"[1/2] Uploading prescription: {file_path}")

    # Upload the prescription file
    uploaded = client.files.upload(file=open(file_path, "rb"))
    file_id = uploaded.id
    print(f"✓ File uploaded: {file_id}")

    print("[2/2] Parsing prescription to markdown with agentic OCR...")

    # Parse with agentic OCR enabled for handwriting
    parse_run = client.parse_runs.create_and_poll(
        file={"id": file_id},
        config={
            "blockOptions": {
                "figures": {
                    "enabled": True,
                    "figureImageClippingEnabled": True,
                    "advancedChartExtractionEnabled": False,
                },
                "text": {
                    "agentic": {
                        "enabled": True,
                    },
                },
                "tables": {
                    "targetFormat": "html",
                    "agentic": {
                        "enabled": False,
                    },
                },
                "barcodes": {
                    "readingEnabled": False,
                },
            },
            "chunkingStrategy": {
                "type": "page",
                "options": {
                    "minCharacters": 500,
                    "maxCharacters": 10000,
                },
            },
        },
    )

    if parse_run.status != "PROCESSED":
        raise Exception(
            f"Parse failed with status: {parse_run.status}. Message: {parse_run.error_message}"
        )

    # Concatenate all chunks into single markdown document
    markdown = "\n\n".join(chunk.content for chunk in parse_run.output.chunks)

    print("✓ Prescription parsed successfully")
    print("\n--- PARSED PRESCRIPTION (MARKDOWN) ---\n")
    print(markdown)
    print("\n--- END PRESCRIPTION ---\n")

    return {
        "fileId": file_id,
        "markdown": markdown,
        "chunkCount": len(parse_run.output.chunks),
    }


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python script.py <prescription-file-path>")
        sys.exit(1)

    file_path = sys.argv[1]
    try:
        result = process_medical_prescriptions_rx(file_path)
        print(f"\n✓ Pipeline complete. Parsed {result['chunkCount']} chunk(s).")
    except Exception as err:
        print(f"✗ Error: {err}")
        sys.exit(1)
// This code uses Extend's REST API directly because Extend has no official Java SDK yet.
// It calls https://api.extend.ai endpoints using only java.net.http.HttpClient (no external dependencies).

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Scanner;

public class MedicalPrescriptionsRx {
  private static final String API_BASE = "https://api.extend.ai";
  private static final String API_KEY = System.getenv("EXTEND_API_KEY");
  private static final HttpClient httpClient = HttpClient.newHttpClient();

  /**
   * Process a medical prescription document:
   * 1. Upload the prescription file
   * 2. Parse to markdown using agentic OCR
   * 3. Return structured markdown for downstream processing
   */
  public static void processMedicalPrescriptionsRx(String filePath) throws Exception {
    // Read file and convert to base64 data URL
    byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
    String base64 = Base64.getEncoder().encodeToString(fileBytes);
    String dataUrl = "data:application/octet-stream;base64," + base64;

    System.out.println("[1/2] Uploading prescription: " + filePath);

    // Upload the prescription file
    String uploadBody = "{\"file\":{\"url\":\"" + escapeJson(dataUrl) + "\"}}";
    HttpRequest uploadRequest = HttpRequest.newBuilder()
        .uri(URI.create(API_BASE + "/files"))
        .header("Authorization", "Bearer " + API_KEY)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(uploadBody))
        .build();

    HttpResponse<String> uploadResponse = httpClient.send(uploadRequest, HttpResponse.BodyHandlers.ofString());
    String fileId = extractJsonField(uploadResponse.body(), "id");
    System.out.println("✓ File uploaded: " + fileId);

    System.out.println("[2/2] Parsing prescription to markdown with agentic OCR...");

    // Parse with agentic OCR enabled for handwriting
    String parseBody = buildParseRequestBody(fileId);
    HttpRequest parseRequest = HttpRequest.newBuilder()
        .uri(URI.create(API_BASE + "/parseRuns/createAndPoll"))
        .header("Authorization", "Bearer " + API_KEY)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(parseBody))
        .build();

    HttpResponse<String> parseResponse = httpClient.send(parseRequest, HttpResponse.BodyHandlers.ofString());
    String status = extractJsonField(parseResponse.body(), "status");

    if (!"PROCESSED".equals(status)) {
      String errorMessage = extractJsonField(parseResponse.body(), "errorMessage");
      throw new Exception("Parse failed with status: " + status + ". Message: " + errorMessage);
    }

    // Extract markdown from chunks
    String markdown = extractMarkdownFromResponse(parseResponse.body());
    int chunkCount = countChunks(parseResponse.body());

    System.out.println("✓ Prescription parsed successfully");
    System.out.println("\n--- PARSED PRESCRIPTION (MARKDOWN) ---\n");
    System.out.println(markdown);
    System.out.println("\n--- END PRESCRIPTION ---\n");

    System.out.println("\n✓ Pipeline complete. Parsed " + chunkCount + " chunk(s).");
  }

  private static String buildParseRequestBody(String fileId) {
    return "{"
        + "\"file\":{\"id\":\"" + fileId + "\"},"
        + "\"config\":{"
        + "\"blockOptions\":{"
        + "\"figures\":{"
        + "\"enabled\":true,"
        + "\"figureImageClippingEnabled\":true,"
        + "\"advancedChartExtractionEnabled\":false"
        + "},"
        + "\"text\":{"
        + "\"agentic\":{\"enabled\":true}"
        + "},"
        + "\"tables\":{"
        + "\"targetFormat\":\"html\","
        + "\"agentic\":{\"enabled\":false}"
        + "},"
        + "\"barcodes\":{\"readingEnabled\":false}"
        + "},"
        + "\"chunkingStrategy\":{"
        + "\"type\":\"page\","
        + "\"options\":{\"minCharacters\":500,\"maxCharacters\":10000}"
        + "}"
        + "}"
        + "}";
  }

  private static String extractJsonField(String json, String fieldName) {
    String searchKey = "\"" + fieldName + "\":";
    int startIdx = json.indexOf(searchKey);
    if (startIdx == -1) return null;
    startIdx += searchKey.length();
    while (startIdx < json.length() && Character.isWhitespace(json.charAt(startIdx))) {
      startIdx++;
    }
    if (json.charAt(startIdx) == '"') {
      startIdx++;
      int endIdx = startIdx;
      while (endIdx < json.length() && json.charAt(endIdx) != '"') {
        if (json.charAt(endIdx) == '\\') endIdx++;
        endIdx++;
      }
      return json.substring(startIdx, endIdx);
    }
    return null;
  }

  private static String extractMarkdownFromResponse(String json) {
    StringBuilder markdown = new StringBuilder();
    int chunksIdx = json.indexOf("\"chunks\":");
    if (chunksIdx == -1) return "";
    int arrayStart = json.indexOf("[", chunksIdx);
    int arrayEnd = json.lastIndexOf("]");
    String chunksArray = json.substring(arrayStart + 1, arrayEnd);
    
    String[] chunks = chunksArray.split("\"content\":");
    for (int i = 1; i < chunks.length; i++) {
      int contentStart = chunks[i].indexOf("\"") + 1;
      int contentEnd = chunks[i].indexOf("\"", contentStart);
      if (contentEnd > contentStart) {
        String content = chunks[i].substring(contentStart, contentEnd);
        content = content.replace("\\n", "\n").replace("\\\"", "\"").replace("\\\\", "\\");
        if (markdown.length() > 0) markdown.append("\n\n");
        markdown.append(content);
      }
    }
    return markdown.toString();
  }

  private static int countChunks(String json) {
    int count = 0;
    int idx = 0;
    while ((idx = json.indexOf("\"content\":", idx)) != -1) {
      count++;
      idx += 10;
    }
    return count;
  }

  private static String escapeJson(String str) {
    return str.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
  }

  public static void main(String[] args) {
    if (args.length == 0) {
      System.err.println("Usage: java MedicalPrescriptionsRx <prescription-file-path>");
      System.exit(1);
    }
    try {
      processMedicalPrescriptionsRx(args[0]);
    } catch (Exception e) {
      System.err.println("✗ Error: " + e.getMessage());
      e.printStackTrace();
      System.exit(1);
    }
  }
}
// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It calls https://api.extend.ai endpoints with standard net/http and encoding/json.

package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"time"
)

const extendAPIBase = "https://api.extend.ai"

type FileUploadRequest struct {
	File FileRef `json:"file"`
}

type FileRef struct {
	URL string `json:"url"`
}

type FileUploadResponse struct {
	ID string `json:"id"`
}

type ParseRunRequest struct {
	File   FileRef    `json:"file"`
	Config ParseConfig `json:"config"`
}

type FileIDRef struct {
	ID string `json:"id"`
}

type ParseConfig struct {
	BlockOptions     BlockOptions     `json:"blockOptions"`
	ChunkingStrategy ChunkingStrategy `json:"chunkingStrategy"`
}

type BlockOptions struct {
	Figures  FiguresConfig `json:"figures"`
	Text     TextConfig    `json:"text"`
	Tables   TablesConfig  `json:"tables"`
	Barcodes BarcodeConfig `json:"barcodes"`
}

type FiguresConfig struct {
	Enabled                      bool `json:"enabled"`
	FigureImageClippingEnabled   bool `json:"figureImageClippingEnabled"`
	AdvancedChartExtractionEnabled bool `json:"advancedChartExtractionEnabled"`
}

type TextConfig struct {
	Agentic AgenticConfig `json:"agentic"`
}

type AgenticConfig struct {
	Enabled bool `json:"enabled"`
}

type TablesConfig struct {
	TargetFormat string        `json:"targetFormat"`
	Agentic      AgenticConfig `json:"agentic"`
}

type BarcodeConfig struct {
	ReadingEnabled bool `json:"readingEnabled"`
}

type ChunkingStrategy struct {
	Type    string                 `json:"type"`
	Options ChunkingStrategyOptions `json:"options"`
}

type ChunkingStrategyOptions struct {
	MinCharacters int `json:"minCharacters"`
	MaxCharacters int `json:"maxCharacters"`
}

type ParseRunResponse struct {
	ID           string      `json:"id"`
	Status       string      `json:"status"`
	ErrorMessage string      `json:"errorMessage"`
	Output       ParseOutput `json:"output"`
}

type ParseOutput struct {
	Chunks []Chunk `json:"chunks"`
}

type Chunk struct {
	Content string `json:"content"`
}

type ProcessResult struct {
	FileID     string
	Markdown   string
	ChunkCount int
}

func processMedicalPrescriptionsRx(filePath string, apiKey string) (*ProcessResult, error) {
	// Read file and convert to base64 data URL
	fileBuffer, err := ioutil.ReadFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %w", err)
	}
	dataURL := fmt.Sprintf("data:application/octet-stream;base64,%s", base64.StdEncoding.EncodeToString(fileBuffer))

	fmt.Printf("[1/2] Uploading prescription: %s\n", filePath)

	// Upload the prescription file
	uploadReq := FileUploadRequest{
		File: FileRef{URL: dataURL},
	}
	uploadBody, err := json.Marshal(uploadReq)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal upload request: %w", err)
	}

	httpReq, err := http.NewRequest("POST", extendAPIBase+"/files", bytes.NewReader(uploadBody))
	if err != nil {
		return nil, fmt.Errorf("failed to create upload request: %w", err)
	}
	httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
	httpReq.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("upload request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		body, _ := ioutil.ReadAll(resp.Body)
		return nil, fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(body))
	}

	var uploadResp FileUploadResponse
	if err := json.NewDecoder(resp.Body).Decode(&uploadResp); err != nil {
		return nil, fmt.Errorf("failed to decode upload response: %w", err)
	}
	fileID := uploadResp.ID
	fmt.Printf("✓ File uploaded: %s\n", fileID)

	fmt.Println("[2/2] Parsing prescription to markdown with agentic OCR...")

	// Create parse run request
	parseReq := ParseRunRequest{
		File: FileIDRef{ID: fileID},
		Config: ParseConfig{
			BlockOptions: BlockOptions{
				Figures: FiguresConfig{
					Enabled:                      true,
					FigureImageClippingEnabled:   true,
					AdvancedChartExtractionEnabled: false,
				},
				Text: TextConfig{
					Agentic: AgenticConfig{Enabled: true},
				},
				Tables: TablesConfig{
					TargetFormat: "html",
					Agentic:      AgenticConfig{Enabled: false},
				},
				Barcodes: BarcodeConfig{ReadingEnabled: false},
			},
			ChunkingStrategy: ChunkingStrategy{
				Type: "page",
				Options: ChunkingStrategyOptions{
					MinCharacters: 500,
					MaxCharacters: 10000,
				},
			},
		},
	}

	parseBody, err := json.Marshal(parseReq)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal parse request: %w", err)
	}

	parseHTTPReq, err := http.NewRequest("POST", extendAPIBase+"/parseRuns", bytes.NewReader(parseBody))
	if err != nil {
		return nil, fmt.Errorf("failed to create parse request: %w", err)
	}
	parseHTTPReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
	parseHTTPReq.Header.Set("Content-Type", "application/json")

	parseResp, err := client.Do(parseHTTPReq)
	if err != nil {
		return nil, fmt.Errorf("parse request failed: %w", err)
	}
	defer parseResp.Body.Close()

	if parseResp.StatusCode != http.StatusOK && parseResp.StatusCode != http.StatusCreated {
		body, _ := ioutil.ReadAll(parseResp.Body)
		return nil, fmt.Errorf("parse request failed with status %d: %s", parseResp.StatusCode, string(body))
	}

	var parseRunResp ParseRunResponse
	if err := json.NewDecoder(parseResp.Body).Decode(&parseRunResp); err != nil {
		return nil, fmt.Errorf("failed to decode parse response: %w", err)
	}

	// Poll for completion
	for parseRunResp.Status != "PROCESSED" && parseRunResp.Status != "FAILED" {
		time.Sleep(2 * time.Second)
		pollReq, err := http.NewRequest("GET", fmt.Sprintf("%s/parseRuns/%s", extendAPIBase, parseRunResp.ID), nil)
		if err != nil {
			return nil, fmt.Errorf("failed to create poll request: %w", err)
		}
		pollReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))

		pollResp, err := client.Do(pollReq)
		if err != nil {
			return nil, fmt.Errorf("poll request failed: %w", err)
		}
		defer pollResp.Body.Close()

		if err := json.NewDecoder(pollResp.Body).Decode(&parseRunResp); err != nil {
			return nil, fmt.Errorf("failed to decode poll response: %w", err)
		}
	}

	if parseRunResp.Status != "PROCESSED" {
		return nil, fmt.Errorf("parse failed with status: %s. Message: %s", parseRunResp.Status, parseRunResp.ErrorMessage)
	}

	// Concatenate all chunks into single markdown document
	var markdown string
	for i, chunk := range parseRunResp.Output.Chunks {
		if i > 0 {
			markdown += "\n\n"
		}
		markdown += chunk.Content
	}

	fmt.Println("✓ Prescription parsed successfully")
	fmt.Println("\n--- PARSED PRESCRIPTION (MARKDOWN) ---\n")
	fmt.Println(markdown)
	fmt.Println("\n--- END PRESCRIPTION ---\n")

	return &ProcessResult{
		FileID:     fileID,
		Markdown:   markdown,
		ChunkCount: len(parseRunResp.Output.Chunks),
	}, nil
}

func main() {
	flag.Parse()
	args := flag.Args()

	if len(args) < 1 {
		fmt.Fprintf(os.Stderr, "Usage: %s <prescription-file-path>\n", os.Args[0])
		os.Exit(1)
	}

	filePath := args[0]
	apiKey := os.Getenv("EXTEND_API_KEY")
	if apiKey == "" {
		fmt.Fprintf(os.Stderr, "Error: EXTEND_API_KEY environment variable not set\n")
		os.Exit(1)
	}

	result, err := processMedicalPrescriptionsRx(filePath, apiKey)
	if err != nil {
		fmt.Fprintf(os.Stderr, "✗ Error: %v\n", err)
		os.Exit(1)
	}

	fmt.Printf("\n✓ Pipeline complete. Parsed %d chunk(s).\n", result.ChunkCount)
}
// Deploy the "Medical Prescriptions Rx" pipeline to YOUR Extend account.
//
// The workflow below is fully self-contained — every EXTRACT/CLASSIFY/SPLIT
// step carries its extractor/classifier/splitter config INLINE, so this is a
// single API call. No processors to create or wire up beforehand.
// Idempotent: the created workflow id is cached in .extend/doctor-s-notes-parser.json,
// so re-running updates the existing workflow instead of duplicating it.
//
// Usage:
//   export EXTEND_API_KEY=sk_...   (from https://dashboard.extend.ai → API Keys)
//   npx tsx provision.ts
//
// Generated by doc1 (template: doctor-s-notes-parser).

import fs from "node:fs";
import path from "node:path";

const API = "https://api.extend.ai";
const VERSION = "2026-02-09";
const API_KEY = process.env.EXTEND_API_KEY;
if (!API_KEY) { console.error("Set EXTEND_API_KEY first."); process.exit(1); }

const STATE_DIR = path.join(process.cwd(), ".extend");
const STATE_FILE = path.join(STATE_DIR, "doctor-s-notes-parser.json");

type State = { workflowId?: string };
const state: State = fs.existsSync(STATE_FILE)
  ? JSON.parse(fs.readFileSync(STATE_FILE, "utf8"))
  : {};
function saveState() {
  fs.mkdirSync(STATE_DIR, { recursive: true });
  fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}

async function api(method: string, pathName: string, body?: unknown) {
  const res = await fetch(API + pathName, {
    method,
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "x-extend-api-version": VERSION,
      ...(body ? { "Content-Type": "application/json" } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(`${method} ${pathName} failed (${res.status}): ${JSON.stringify(data).slice(0, 300)}`);
  return data;
}

// ── Workflow definition — extractor/classifier/splitter configs inline ──────
const WORKFLOW = {
  "name": "Medical Prescriptions Rx Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "figures": {
              "enabled": true,
              "figureImageClippingEnabled": true,
              "advancedChartExtractionEnabled": false,
              "customInstructions": ""
            },
            "text": {
              "signatureDetectionEnabled": false,
              "agentic": {
                "enabled": true
              }
            },
            "tables": {
              "targetFormat": "html",
              "tableHeaderContinuationEnabled": false,
              "cellBlocksEnabled": false,
              "agentic": {
                "enabled": false
              }
            },
            "barcodes": {
              "imageClippingEnabled": false,
              "readingEnabled": false
            },
            "keyValue": {
              "blankFieldFormattingEnabled": false
            },
            "formulas": {
              "enabled": false
            }
          },
          "chunkingStrategy": {
            "type": "page",
            "options": {
              "minCharacters": 500,
              "maxCharacters": 10000
            }
          }
        }
      }
    }
  ]
};

async function main() {
  console.log(`Deploying "${WORKFLOW.name}"…`);

  if (state.workflowId) {
    console.log(`✓ workflow already provisioned (${state.workflowId}) — updating steps`);
    await api("POST", `/workflows/${state.workflowId}`, { steps: WORKFLOW.steps });
  } else {
    // Reuse an existing workflow with the same name if one exists (e.g. a
    // previous run's state file was lost) instead of creating a duplicate.
    try {
      const list = await api("GET", `/workflows?name=${encodeURIComponent(WORKFLOW.name)}`);
      const items = (list.data ?? list.items ?? []) as Array<{ name?: string; id?: string }>;
      const existing = items.find((x) => x.name === WORKFLOW.name);
      if (existing?.id) {
        state.workflowId = existing.id; saveState();
        console.log(`✓ workflow "${WORKFLOW.name}" found in your account (${existing.id}) — updating steps`);
        await api("POST", `/workflows/${existing.id}`, { steps: WORKFLOW.steps });
      }
    } catch { /* lookup is best-effort; fall through to create */ }

    if (!state.workflowId) {
      const created = await api("POST", "/workflows", WORKFLOW);
      const wfId = created.id ?? created.workflow?.id;
      if (!wfId) throw new Error("Could not read created workflow id from response");
      state.workflowId = wfId; saveState();
      console.log(`+ created workflow (${wfId})`);
    }
  }

  // Deploy the current draft as a new version so the workflow is runnable —
  // best-effort: some accounts/plans may not require this explicit step.
  await api("POST", `/workflows/${state.workflowId}/versions`, {}).catch(() => {});

  console.log("\nDone. Run documents through it with:");
  console.log(`  POST ${API}/workflow_runs  { workflow: { id: "${state.workflowId}" }, file: { url: "https://…" } }`);
  console.log("Or open the workflow in the Extend dashboard to review and deploy it.");
}

main().catch((e) => { console.error(e.message ?? e); process.exit(1); });
import os
import json
import sys
from pathlib import Path
from urllib.parse import urlencode

from extend_ai import Extend

API = "https://api.extend.ai"
VERSION = "2026-02-09"
API_KEY = os.environ.get("EXTEND_API_KEY")

if not API_KEY:
    print("Set EXTEND_API_KEY first.", file=sys.stderr)
    sys.exit(1)

STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "doctor-s-notes-parser.json"

state: dict = {}
if STATE_FILE.exists():
    state = json.loads(STATE_FILE.read_text())


def save_state() -> None:
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, indent=2))


WORKFLOW = {
    "name": "Medical Prescriptions Rx Processing Pipeline",
    "steps": [
        {
            "name": "startTrigger1",
            "type": "TRIGGER",
            "next": [{"step": "parse1"}],
        },
        {
            "name": "parse1",
            "type": "PARSE",
            "config": {
                "parseConfig": {
                    "blockOptions": {
                        "figures": {
                            "enabled": True,
                            "figureImageClippingEnabled": True,
                            "advancedChartExtractionEnabled": False,
                            "customInstructions": "",
                        },
                        "text": {
                            "signatureDetectionEnabled": False,
                            "agentic": {"enabled": True},
                        },
                        "tables": {
                            "targetFormat": "html",
                            "tableHeaderContinuationEnabled": False,
                            "cellBlocksEnabled": False,
                            "agentic": {"enabled": False},
                        },
                        "barcodes": {
                            "imageClippingEnabled": False,
                            "readingEnabled": False,
                        },
                        "keyValue": {"blankFieldFormattingEnabled": False},
                        "formulas": {"enabled": False},
                    },
                    "chunkingStrategy": {
                        "type": "page",
                        "options": {"minCharacters": 500, "maxCharacters": 10000},
                    },
                }
            },
        },
    ],
}


def main() -> None:
    client = Extend(token=API_KEY)

    print(f'Deploying "{WORKFLOW["name"]}…"')

    if state.get("workflowId"):
        workflow_id = state["workflowId"]
        print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
        client.workflows.update(id=workflow_id, steps=WORKFLOW["steps"])
    else:
        # Reuse an existing workflow with the same name if one exists
        try:
            workflows_list = client.workflows.list(name=WORKFLOW["name"])
            items = workflows_list.data if hasattr(workflows_list, "data") else []
            existing = next(
                (w for w in items if w.name == WORKFLOW["name"]), None
            )
            if existing and existing.id:
                state["workflowId"] = existing.id
                save_state()
                print(
                    f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing.id}) — updating steps'
                )
                client.workflows.update(id=existing.id, steps=WORKFLOW["steps"])
        except Exception:
            pass

        if not state.get("workflowId"):
            created = client.workflows.create(**WORKFLOW)
            workflow_id = created.id
            if not workflow_id:
                raise ValueError("Could not read created workflow id from response")
            state["workflowId"] = workflow_id
            save_state()
            print(f"+ created workflow ({workflow_id})")

    # Deploy the current draft as a new version so the workflow is runnable
    try:
        client.workflows.create_version(id=state["workflowId"])
    except Exception:
        pass

    workflow_id = state["workflowId"]
    print("\nDone. Run documents through it with:")
    print(
        f'  POST {API}/workflow_runs  {{ workflow: {{ id: "{workflow_id}" }}, file: {{ url: "https://…" }} }}'
    )
    print("Or open the workflow in the Extend dashboard to review and deploy it.")


if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(str(e), file=sys.stderr)
        sys.exit(1)
// This code uses Extend's REST API directly because Extend has no official Java SDK yet.
// It calls https://api.extend.ai endpoints with Bearer token authentication.

import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class ProvisionMedicalPrescriptionsRx {
  private static final String API = "https://api.extend.ai";
  private static final String VERSION = "2026-02-09";
  private static final String API_KEY = System.getenv("EXTEND_API_KEY");
  private static final String STATE_DIR = Paths.get(System.getProperty("user.dir"), ".extend").toString();
  private static final String STATE_FILE = Paths.get(STATE_DIR, "doctor-s-notes-parser.json").toString();

  private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();

  static class State {
    String workflowId;
  }

  private static State state = new State();

  public static void main(String[] args) {
    try {
      if (API_KEY == null || API_KEY.isEmpty()) {
        System.err.println("Set EXTEND_API_KEY first.");
        System.exit(1);
      }

      loadState();

      Map<String, Object> workflow = buildWorkflow();
      String workflowName = (String) workflow.get("name");

      System.out.println("Deploying \"" + workflowName + "\"…");

      if (state.workflowId != null && !state.workflowId.isEmpty()) {
        System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
        Map<String, Object> updateBody = new LinkedHashMap<>();
        updateBody.put("steps", workflow.get("steps"));
        api("POST", "/workflows/" + state.workflowId, updateBody);
      } else {
        try {
          String encodedName = URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
          Map<String, Object> listResponse = api("GET", "/workflows?name=" + encodedName, null);
          List<?> items = (List<?>) listResponse.getOrDefault("data", listResponse.getOrDefault("items", List.of()));

          for (Object item : items) {
            if (item instanceof Map) {
              Map<?, ?> itemMap = (Map<?, ?>) item;
              if (workflowName.equals(itemMap.get("name"))) {
                String existingId = (String) itemMap.get("id");
                if (existingId != null && !existingId.isEmpty()) {
                  state.workflowId = existingId;
                  saveState();
                  System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + existingId + ") — updating steps");
                  Map<String, Object> updateBody = new LinkedHashMap<>();
                  updateBody.put("steps", workflow.get("steps"));
                  api("POST", "/workflows/" + existingId, updateBody);
                  break;
                }
              }
            }
          }
        } catch (Exception e) {
          // lookup is best-effort; fall through to create
        }

        if (state.workflowId == null || state.workflowId.isEmpty()) {
          Map<String, Object> created = api("POST", "/workflows", workflow);
          String wfId = (String) created.get("id");
          if (wfId == null || wfId.isEmpty()) {
            Map<?, ?> workflowObj = (Map<?, ?>) created.get("workflow");
            if (workflowObj != null) {
              wfId = (String) workflowObj.get("id");
            }
          }
          if (wfId == null || wfId.isEmpty()) {
            throw new RuntimeException("Could not read created workflow id from response");
          }
          state.workflowId = wfId;
          saveState();
          System.out.println("+ created workflow (" + wfId + ")");
        }
      }

      try {
        api("POST", "/workflows/" + state.workflowId + "/versions", new LinkedHashMap<>());
      } catch (Exception e) {
        // best-effort: some accounts/plans may not require this explicit step
      }

      System.out.println("\nDone. Run documents through it with:");
      System.out.println("  POST " + API + "/workflow_runs  { workflow: { id: \"" + state.workflowId + "\" }, file: { url: \"https://…\" } }");
      System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
    } catch (Exception e) {
      System.err.println(e.getMessage() != null ? e.getMessage() : e.toString());
      System.exit(1);
    }
  }

  private static void loadState() throws IOException {
    Path stateFilePath = Paths.get(STATE_FILE);
    if (Files.exists(stateFilePath)) {
      String content = Files.readString(stateFilePath);
      Map<String, Object> parsed = parseJson(content);
      state.workflowId = (String) parsed.get("workflowId");
    }
  }

  private static void saveState() throws IOException {
    Path stateDirPath = Paths.get(STATE_DIR);
    Files.createDirectories(stateDirPath);
    Map<String, Object> stateMap = new LinkedHashMap<>();
    if (state.workflowId != null) {
      stateMap.put("workflowId", state.workflowId);
    }
    String json = toJson(stateMap);
    Files.writeString(Paths.get(STATE_FILE), json);
  }

  private static Map<String, Object> api(String method, String pathName, Map<String, Object> body) throws IOException, InterruptedException {
    HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
        .uri(URI.create(API + pathName))
        .header("Authorization", "Bearer " + API_KEY)
        .header("x-extend-api-version", VERSION);

    if (body != null) {
      String jsonBody = toJson(body);
      requestBuilder.header("Content-Type", "application/json")
          .method(method, HttpRequest.BodyPublishers.ofString(jsonBody));
    } else {
      requestBuilder.method(method, HttpRequest.BodyPublishers.noBody());
    }

    HttpRequest request = requestBuilder.build();
    HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());

    Map<String, Object> data;
    try {
      data = parseJson(response.body());
    } catch (Exception e) {
      data = new LinkedHashMap<>();
    }

    if (response.statusCode() < 200 || response.statusCode() >= 300) {
      String errorMsg = toJson(data);
      if (errorMsg.length() > 300) {
        errorMsg = errorMsg.substring(0, 300);
      }
      throw new RuntimeException(method + " " + pathName + " failed (" + response.statusCode() + "): " + errorMsg);
    }

    return data;
  }

  private static Map<String, Object> buildWorkflow() {
    Map<String, Object> workflow = new LinkedHashMap<>();
    workflow.put("name", "Medical Prescriptions Rx Processing Pipeline");

    Map<String, Object> trigger = new LinkedHashMap<>();
    trigger.put("name", "startTrigger1");
    trigger.put("type", "TRIGGER");
    Map<String, Object> nextStep = new LinkedHashMap<>();
    nextStep.put("step", "parse1");
    trigger.put("next", List.of(nextStep));

    Map<String, Object> parseStep = new LinkedHashMap<>();
    parseStep.put("name", "parse1");
    parseStep.put("type", "PARSE");

    Map<String, Object> config = new LinkedHashMap<>();
    Map<String, Object> parseConfig = new LinkedHashMap<>();
    Map<String, Object> blockOptions = new LinkedHashMap<>();

    Map<String, Object> figures = new LinkedHashMap<>();
    figures.put("enabled", true);
    figures.put("figureImageClippingEnabled", true);
    figures.put("advancedChartExtractionEnabled", false);
    figures.put("customInstructions", "");
    blockOptions.put("figures", figures);

    Map<String, Object> text = new LinkedHashMap<>();
    Map<String, Object> textAgentic = new LinkedHashMap<>();
    textAgentic.put("enabled", true);
    text.put("agentic", textAgentic);
    text.put("signatureDetectionEnabled", false);
    blockOptions.put("text", text);

    Map<String, Object> tables = new LinkedHashMap<>();
    tables.put("targetFormat", "html");
    tables.put("tableHeaderContinuationEnabled", false);
    tables.put("cellBlocksEnabled", false);
    Map<String, Object> tablesAgentic = new LinkedHashMap<>();
    tablesAgentic.put("enabled", false);
    tables.put("agentic", tablesAgentic);
    blockOptions.put("tables", tables);

    Map<String, Object> barcodes = new LinkedHashMap<>();
    barcodes.put("imageClippingEnabled", false);
    barcodes.put("readingEnabled", false);
    blockOptions.put("barcodes", barcodes);

    Map<String, Object> keyValue = new LinkedHashMap<>();
    keyValue.put("blankFieldFormattingEnabled", false);
    blockOptions.put("keyValue", keyValue);

    Map<String, Object> formulas = new LinkedHashMap<>();
    formulas.put("enabled", false);
    blockOptions.put("formulas", formulas);

    parseConfig.put("blockOptions", blockOptions);

    Map<String, Object> chunkingStrategy = new LinkedHashMap<>();
    chunkingStrategy.put("type", "page");
    Map<String, Object> chunkingOptions = new LinkedHashMap<>();
    chunkingOptions.put("minCharacters", 500);
    chunkingOptions.put("maxCharacters", 10000);
    chunkingStrategy.put("options", chunkingOptions);
    parseConfig.put("chunkingStrategy", chunkingStrategy);

    config.put("parseConfig", parseConfig);
    parseStep.put("config", config);

    workflow.put("steps", List.of(trigger, parseStep));

    return workflow;
  }

  private static String toJson(Object obj) {
    if (obj == null) return "null";
    if (obj instanceof String) return "\"" + escapeJson((String) obj) + "\"";
    if (obj instanceof Number) return obj.toString();
    if (obj instanceof Boolean) return obj.toString();
    if (obj instanceof Map) {
      Map<?, ?> map = (Map<?, ?>) obj;
      StringBuilder sb = new StringBuilder("{");
      boolean first = true;
      for (Map.Entry<?, ?> entry : map.entrySet()) {
        if (!first) sb.append(",");
        sb.append("\"").append(escapeJson(entry.getKey().toString())).append("\":");
        sb.append(toJson(entry.getValue()));
        first = false;
      }
      sb.append("}");
      return sb.toString();
    }
    if (obj instanceof List) {
      List<?> list = (List<?>) obj;
      StringBuilder sb = new StringBuilder("[");
      boolean first = true;
      for (Object item : list) {
        if (!first) sb.append(",");
        sb.append(toJson(item));
        first = false;
      }
      sb.append("]");
      return sb.toString();
    }
    return "null";
  }

  private static String escapeJson(String s) {
    return s.replace("\\", "\\\\")
        .replace("\"", "\\\"")
        .replace("\n", "\\n")
        .replace("\r", "\\r")
        .replace("\t", "\\t");
  }

  private static Map<String, Object> parseJson(String json) {
    json = json.trim();
    if (!json.startsWith("{")) return new LinkedHashMap<>();
    return parseJsonObject(json, new int[]{0});
  }

  private static Map<String, Object> parseJsonObject(String json, int[] pos) {
    Map<String, Object> map = new LinkedHashMap<>();
    pos[0]++; // skip '{'
    skipWhitespace(json, pos);
    if (pos[0] < json.length() && json.charAt(pos[0]) == '}') {
      pos[0]++;
      return map;
    }
    while (pos[0] < json.length()) {
      skipWhitespace(json, pos);
      String key = parseJsonString(json, pos);
      skipWhitespace(json, pos);
      if (pos[0] < json.length() && json.charAt(pos[0]) == ':') {
        pos[0]++;
      }
      skipWhitespace(json, pos);
      Object value = parseJsonValue(json, pos);
      map.put(key, value);
      skipWhitespace(json, pos);
      if (pos[0] < json.length() && json.charAt(pos[0]) == ',') {
        pos[0]++;
      } else {
        break;
      }
    }
    if (pos[0] < json.length() && json.charAt(pos[0]) == '}') {
      pos[0]++;
    }
    return map;
  }

  private static List<?> parseJsonArray(String json, int[] pos) {
    List<Object> list = new java.util.ArrayList<>();
    pos[0]++; // skip '['
    skipWhitespace(json, pos);
    if (pos[0] < json.length() && json.charAt(pos[0]) == ']') {
      pos[0]++;
      return list;
    }
    while (pos[0] < json.length()) {
      skipWhitespace(json, pos);
      Object value = parseJsonValue(json, pos);
      list.add(value);
      skipWhitespace(json, pos);
      if (pos[0] < json.length() && json.charAt(pos[0]) == ',') {
        pos[0]++;
      } else {
        break;
      }
    }
    if (pos[0] < json.length() && json.charAt(pos[0]) == ']') {
      pos[0]++;
    }
    return list;
  }

  private static Object parseJsonValue(String json, int[] pos) {
    skipWhitespace(json, pos);
    if (pos[0] >= json.length()) return null;
    char c = json.charAt(pos[0]);
    if (c == '"') return parseJsonString(json, pos);
    if (c == '{') return parseJsonObject(json, pos);
    if (c == '[') return parseJsonArray(json, pos);
    if (c == 't' || c == 'f') {
      if (json.startsWith("true", pos[0])) {
        pos[0] += 4;
        return true;
      }
      if (json.startsWith("false", pos[0])) {
        pos[0] += 5;
        return false;
      }
    }
    if (c == 'n') {
      if (json.startsWith("null", pos[0])) {
        pos[0] += 4;
        return null;
      }
    }
    if (c == '-' || Character.isDigit(c)) {
      int start = pos[0];
      if (c == '-') pos[0]++;
      while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) pos[0]++;
      if (pos[0] < json.length() && json.charAt(pos[0]) == '.') {
        pos[0]++;
        while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) pos[0]++;
      }
      String numStr = json.substring(start, pos[0]);
      try {
        if (numStr.contains(".")) return Double.parseDouble(numStr);
        return Long.parseLong(numStr);
      } catch (NumberFormatException e) {
        return numStr;
      }
    }
    return null;
  }

  private static String parseJsonString(String json, int[] pos) {
    pos[0]++; // skip opening '"'
    StringBuilder sb = new StringBuilder();
    while (pos[0] < json.length()) {
      char c = json.charAt(pos[0]);
      if (c == '"') {
        pos[0]++;
        break;
      }
      if (c == '\\' && pos[0] + 1 < json.length()) {
        pos[0]++;
        char next = json.charAt(pos[0]);
        switch (next) {
          case '"': sb.append('"'); break;
          case '\\': sb.append('\\'); break;
          case '/': sb.append('/'); break;
          case 'b': sb.append('\b'); break;
          case 'f': sb.append('\f'); break;
          case 'n': sb.append('\n'); break;
          case 'r': sb.append('\r'); break;
          case 't': sb.append('\t'); break;
          default: sb.append(next);
        }
      } else {
        sb.append(c);
      }
      pos[0]++;
    }
    return sb.toString();
  }

  private static void skipWhitespace(String json, int[] pos) {
    while (pos[0] < json.length() && Character.isWhitespace(json.charAt(pos[0]))) {
      pos[0]++;
    }
  }
}
// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It deploys the "Medical Prescriptions Rx" pipeline to your Extend account.
//
// Usage:
//   export EXTEND_API_KEY=sk_...   (from https://dashboard.extend.ai → API Keys)
//   go run provision.go
//
// Generated by doc1 (template: doctor-s-notes-parser).

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
)

const (
	API     = "https://api.extend.ai"
	VERSION = "2026-02-09"
)

var (
	apiKey   string
	stateDir string
	stateFile string
)

type State struct {
	WorkflowID string `json:"workflowId,omitempty"`
}

type BlockOptions struct {
	Figures struct {
		Enabled                      bool   `json:"enabled"`
		FigureImageClippingEnabled   bool   `json:"figureImageClippingEnabled"`
		AdvancedChartExtractionEnabled bool `json:"advancedChartExtractionEnabled"`
		CustomInstructions           string `json:"customInstructions"`
	} `json:"figures"`
	Text struct {
		SignatureDetectionEnabled bool `json:"signatureDetectionEnabled"`
		Agentic struct {
			Enabled bool `json:"enabled"`
		} `json:"agentic"`
	} `json:"text"`
	Tables struct {
		TargetFormat                   string `json:"targetFormat"`
		TableHeaderContinuationEnabled bool   `json:"tableHeaderContinuationEnabled"`
		CellBlocksEnabled              bool   `json:"cellBlocksEnabled"`
		Agentic struct {
			Enabled bool `json:"enabled"`
		} `json:"agentic"`
	} `json:"tables"`
	Barcodes struct {
		ImageClippingEnabled bool `json:"imageClippingEnabled"`
		ReadingEnabled       bool `json:"readingEnabled"`
	} `json:"barcodes"`
	KeyValue struct {
		BlankFieldFormattingEnabled bool `json:"blankFieldFormattingEnabled"`
	} `json:"keyValue"`
	Formulas struct {
		Enabled bool `json:"enabled"`
	} `json:"formulas"`
}

type ChunkingStrategy struct {
	Type    string `json:"type"`
	Options struct {
		MinCharacters int `json:"minCharacters"`
		MaxCharacters int `json:"maxCharacters"`
	} `json:"options"`
}

type ParseConfig struct {
	BlockOptions      BlockOptions      `json:"blockOptions"`
	ChunkingStrategy  ChunkingStrategy  `json:"chunkingStrategy"`
}

type StepConfig struct {
	ParseConfig ParseConfig `json:"parseConfig"`
}

type Next struct {
	Step string `json:"step"`
}

type WorkflowStep struct {
	Name   string      `json:"name"`
	Type   string      `json:"type"`
	Next   []Next      `json:"next,omitempty"`
	Config StepConfig  `json:"config,omitempty"`
}

type Workflow struct {
	Name  string         `json:"name"`
	Steps []WorkflowStep `json:"steps"`
}

type WorkflowResponse struct {
	ID       string `json:"id,omitempty"`
	Workflow struct {
		ID string `json:"id,omitempty"`
	} `json:"workflow,omitempty"`
}

type WorkflowListResponse struct {
	Data  []WorkflowItem `json:"data,omitempty"`
	Items []WorkflowItem `json:"items,omitempty"`
}

type WorkflowItem struct {
	Name string `json:"name,omitempty"`
	ID   string `json:"id,omitempty"`
}

func init() {
	apiKey = os.Getenv("EXTEND_API_KEY")
	if apiKey == "" {
		fmt.Fprintf(os.Stderr, "Set EXTEND_API_KEY first.\n")
		os.Exit(1)
	}

	cwd, err := os.Getwd()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get working directory: %v\n", err)
		os.Exit(1)
	}
	stateDir = filepath.Join(cwd, ".extend")
	stateFile = filepath.Join(stateDir, "doctor-s-notes-parser.json")
}

func loadState() State {
	data, err := os.ReadFile(stateFile)
	if err != nil {
		return State{}
	}
	var s State
	json.Unmarshal(data, &s)
	return s
}

func saveState(s State) error {
	if err := os.MkdirAll(stateDir, 0755); err != nil {
		return err
	}
	data, err := json.MarshalIndent(s, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(stateFile, data, 0644)
}

func apiCall(method, pathName string, body interface{}) (map[string]interface{}, error) {
	var reqBody io.Reader
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		reqBody = bytes.NewReader(data)
	}

	req, err := http.NewRequest(method, API+pathName, reqBody)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
	req.Header.Set("x-extend-api-version", VERSION)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var data map[string]interface{}
	json.Unmarshal(respBody, &data)

	if resp.StatusCode >= 400 {
		respStr := string(respBody)
		if len(respStr) > 300 {
			respStr = respStr[:300]
		}
		return nil, fmt.Errorf("%s %s failed (%d): %s", method, pathName, resp.StatusCode, respStr)
	}

	return data, nil
}

func buildWorkflow() Workflow {
	blockOpts := BlockOptions{}
	blockOpts.Figures.Enabled = true
	blockOpts.Figures.FigureImageClippingEnabled = true
	blockOpts.Figures.AdvancedChartExtractionEnabled = false
	blockOpts.Figures.CustomInstructions = ""

	blockOpts.Text.SignatureDetectionEnabled = false
	blockOpts.Text.Agentic.Enabled = true

	blockOpts.Tables.TargetFormat = "html"
	blockOpts.Tables.TableHeaderContinuationEnabled = false
	blockOpts.Tables.CellBlocksEnabled = false
	blockOpts.Tables.Agentic.Enabled = false

	blockOpts.Barcodes.ImageClippingEnabled = false
	blockOpts.Barcodes.ReadingEnabled = false

	blockOpts.KeyValue.BlankFieldFormattingEnabled = false

	blockOpts.Formulas.Enabled = false

	chunking := ChunkingStrategy{}
	chunking.Type = "page"
	chunking.Options.MinCharacters = 500
	chunking.Options.MaxCharacters = 10000

	parseConfig := ParseConfig{
		BlockOptions:     blockOpts,
		ChunkingStrategy: chunking,
	}

	stepConfig := StepConfig{
		ParseConfig: parseConfig,
	}

	parse1 := WorkflowStep{
		Name:   "parse1",
		Type:   "PARSE",
		Config: stepConfig,
	}

	trigger := WorkflowStep{
		Name: "startTrigger1",
		Type: "TRIGGER",
		Next: []Next{
			{Step: "parse1"},
		},
	}

	return Workflow{
		Name:  "Medical Prescriptions Rx Processing Pipeline",
		Steps: []WorkflowStep{trigger, parse1},
	}
}

func main() {
	state := loadState()
	workflow := buildWorkflow()

	fmt.Printf("Deploying \"%s\"…\n", workflow.Name)

	if state.WorkflowID != "" {
		fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
		_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", state.WorkflowID), map[string]interface{}{"steps": workflow.Steps})
		if err != nil {
			fmt.Fprintf(os.Stderr, "%v\n", err)
			os.Exit(1)
		}
	} else {
		// Try to find existing workflow with same name
		query := url.QueryEscape(workflow.Name)
		list, err := apiCall("GET", fmt.Sprintf("/workflows?name=%s", query), nil)
		if err == nil {
			var items []WorkflowItem
			if data, ok := list["data"].([]interface{}); ok {
				for _, item := range data {
					if m, ok := item.(map[string]interface{}); ok {
						var wi WorkflowItem
						b, _ := json.Marshal(m)
						json.Unmarshal(b, &wi)
						items = append(items, wi)
					}
				}
			} else if items_raw, ok := list["items"].([]interface{}); ok {
				for _, item := range items_raw {
					if m, ok := item.(map[string]interface{}); ok {
						var wi WorkflowItem
						b, _ := json.Marshal(m)
						json.Unmarshal(b, &wi)
						items = append(items, wi)
					}
				}
			}

			for _, item := range items {
				if item.Name == workflow.Name && item.ID != "" {
					state.WorkflowID = item.ID
					saveState(state)
					fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflow.Name, item.ID)
					_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", item.ID), map[string]interface{}{"steps": workflow.Steps})
					if err != nil {
						fmt.Fprintf(os.Stderr, "%v\n", err)
						os.Exit(1)
					}
					break
				}
			}
		}

		if state.WorkflowID == "" {
			created, err := apiCall("POST", "/workflows", workflow)
			if err != nil {
				fmt.Fprintf(os.Stderr, "%v\n", err)
				os.Exit(1)
			}

			wfID := ""
			if id, ok := created["id"].(string); ok {
				wfID = id
			} else if wf, ok := created["workflow"].(map[string]interface{}); ok {
				if id, ok := wf["id"].(string); ok {
					wfID = id
				}
			}

			if wfID == "" {
				fmt.Fprintf(os.Stderr, "Could not read created workflow id from response\n")
				os.Exit(1)
			}

			state.WorkflowID = wfID
			saveState(state)
			fmt.Printf("+ created workflow (%s)\n", wfID)
		}
	}

	// Deploy the current draft as a new version (best-effort)
	apiCall("POST", fmt.Sprintf("/workflows/%s/versions", state.WorkflowID), map[string]interface{}{})

	fmt.Println("\nDone. Run documents through it with:")
	fmt.Printf("  POST %s/workflow_runs  { workflow: { id: \"%s\" }, file: { url: \"https://…\" } }\n", API, state.WorkflowID)
	fmt.Println("Or open the workflow in the Extend dashboard to review and deploy it.")
}

Frequently Asked Questions (FAQ)

Use async `parseRuns.createAndPoll()` with `mode: "agentic_ocr"` and set `baseProcessor: "parse_performance"` for batch; this trades latency for precision and scales to 100s of scripts. For real-time single-script processing, use sync `extract()` but accept 2–5 second response times.
Tags
Medical PrescriptionPatient DemographicsMedication DosagePhysician Credentials
About this template

This template captures essential data from medical prescriptions including patient demographics, prescribed medications with dosage instructions, and physician credentials. It handles handwritten signatures, license numbers, and specific medication details required for pharmacy fulfillment.

Document formats
  • PDF
  • Images & Scans
Requirements
  • Handwriting
  • Scanned documents