Extracts patient information and medication details from medical prescriptions.
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.
Converts the document into clean, layout-aware markdown plus structured blocks with spatial metadata.
advancedOptions.agenticOcrEnabledfalsechangedadvancedOptions.enrichmentFormat"xml"changedadvancedOptions.excelIncludeCellFormattingfalsechangedadvancedOptions.excelIncludeCellMetadatafalsechangedadvancedOptions.excelParsingMode"advanced"changedadvancedOptions.excelSkipCalculationfalsechangedadvancedOptions.excelSkipHiddenContentfalsechangedadvancedOptions.excelUseRawCellValuesfalsechangedadvancedOptions.pageBreaksEnabledfalsechangedadvancedOptions.pageRotationEnabledtruechangedadvancedOptions.verticalGroupingThreshold1changedblockOptions.barcodes.imageClippingEnabledfalsechangedblockOptions.barcodes.readingEnabledfalsechangedblockOptions.figures.advancedChartExtractionEnabledfalsechangedblockOptions.figures.customInstructions""changedblockOptions.figures.enabledtruechangedblockOptions.figures.figureImageClippingEnabledtruechangedblockOptions.formulas.enabledfalsechangedblockOptions.keyValue.blankFieldFormattingEnabledfalsechangedblockOptions.tables.agentic.enabledfalsechangedblockOptions.tables.cellBlocksEnabledfalsechangedblockOptions.tables.tableHeaderContinuationEnabledfalsechangedblockOptions.tables.targetFormat"html"changedblockOptions.text.agentic.enabledtruechangedblockOptions.text.signatureDetectionEnabledfalsechangedchunkingStrategy.options.maxCharacters10000changedchunkingStrategy.options.minCharacters500changedchunkingStrategy.type"page"changedengineVersion"2.0.0"changedtarget"markdown"changedengine"parse_performance"You can learn more about Parse configuration in Extend's Parse documentation.
{
"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.")
}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.