Financial & BankingParse

W-2 Parser

Turns W-2 forms into markdown, including wage, tax withholding, and earnings data.

Ship it with Extend

Live pipeline

a real document, processed end to end · view only
Source documentW2.png

Step-by-step

A W-2 tax form is an IRS document issued by employers that reports an employee's annual wages, tips, and other compensation along with federal, state, and local tax withholdings and Social Security and Medicare tax information. This template takes in W-2 Tax Forms and outputs markdown (.md) capturing the form's full text and layout by using Extend's Parse primitives.

Input
W-2 Tax Forms
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
chunks[{"id":"chunk_1_r7eiYR","type":"page","blocks":[{"id":"block_1_qPIKMZ","type":"heading","object":"block","content":"# 2025 W-2 and EARNINGS SUMMARY","details":{…changed
parseOutputMetadata.finalMimeType"image/png"changed
parseOutputMetadata.originalMimeType"image/png"changed
parseOutputMetadata.pagesnullchanged
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": "W-2 Tax Form Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              },
              "signatureDetectionEnabled": true
            },
            "tables": {
              "agentic": {
                "enabled": true
              },
              "tableHeaderContinuationEnabled": true
            },
            "figures": {
              "enabled": true
            }
          },
          "chunkingStrategy": {
            "type": "page",
            "options": {}
          }
        }
      }
    }
  ]
}
# W-2 Tax Form Processing — Extend AI Skill

## What this pipeline does

Parses a W-2 tax form (IRS Form W-2) from PDF or image to extract structured markdown with detected text, tables, figures, and form fields. Outputs a machine-readable representation of all wage, withholding, and employee/employer identity data across multiple filing copies (employee, federal, state, local). The parser uses agentic OCR to handle form layouts, checkboxes, and multi-box number fields accurately.

## When to use this

- **Bulk W-2 processing for payroll systems**: Ingest hundreds of W-2s from employees or employers, extract key fields (SSN, wages, withholdings) into a database, then validate against tax records.
- **Tax compliance & audit workflows**: Parse W-2s as part of a compliance review pipeline; markdown output feeds into downstream NLP analysis or manual review queues.
- **HR record digitization**: Convert paper W-2 archives into searchable, structured markdown; preserve bounding boxes for document reconstruction or signature verification.
- **Third-party document aggregation**: Income verification platforms that need to parse W-2s from multiple employers in a single batch; markdown is ideal for RAG + classification chains.
- **Testing OCR quality on tax forms**: W-2s are highly structured; use this pipeline to validate agentic OCR settings before rolling out to other IRS forms (1040, Schedule C, etc.).

## Processor pipeline

### Step 1: Parse (agentic OCR mode, page-chunked)
**Processor**: `parseRuns.createAndPoll({ ... })`  
**Purpose**: Convert W-2 PDF/image to markdown + block-level bounding boxes, preserving form structure.

**Key config choices**:
- **`blockOptions.text.agentic.enabled: true`** — Enables agentic OCR, critical for recognizing form field labels (e.g., "1 Wages, tips, other comp.") and handling variable font sizes, italics, and form-specific typography.
- **`blockOptions.text.signatureDetectionEnabled: true`** — Detects and flags signature blocks (e.g., authorized officer signatures on employer copy); helpful for compliance workflows that require signature presence checks.
- **`blockOptions.tables.agentic.enabled: true`** — Recognizes the complex table structure in Box 12 (multiple rows with inline codes: "E", "W", etc.) and the checkbox grid (Box 13: "Stat emp.", "Ret. plan", "3rd party sick pay").
- **`blockOptions.tables.tableHeaderContinuationEnabled: true`** — Ensures multi-page W-2 tables (if present) maintain header context across page breaks; W-2s are typically single-page, but this is defensive.
- **`blockOptions.figures.enabled: true`** — Captures QR codes, barcodes, or employer logos; useful if downstream systems need to validate issuing employer or route documents.
- **`chunkingStrategy.type: "page"`** — W-2s are typically 1–2 pages; page-level chunking avoids artificial mid-form splits and keeps employer/employee info in same chunk.

**Why these choices**: W-2 forms have dense, tightly spaced fields, mixed typography, embedded tables (Box 12), and checkboxes. Agentic OCR is essential to parse these reliably. Signature detection supports compliance checks. Table agentic mode correctly interprets the multi-code structure in Box 12 (e.g., "E | 4107.00" as code + amount).

## TypeScript implementation



## CLI equivalent

```bash
# Export your API token
export EXTEND_API_KEY="sk_..."

# Parse a W-2 file to markdown
extend parse w2_form.pdf

# Equivalent: using the configuration file below
extend parse w2_form.pdf --config w2_parse_config.json
```

**`w2_parse_config.json`**:
```json
{
  "blockOptions": {
    "text": {
      "agentic": {
        "enabled": true
      },
      "signatureDetectionEnabled": true
    },
    "tables": {
      "agentic": {
        "enabled": true
      },
      "tableHeaderContinuationEnabled": true
    },
    "figures": {
      "enabled": true
    }
  },
  "chunkingStrategy": {
    "type": "page",
    "options": {}
  }
}
```

Then invoke:
```bash
extend parse w2_form.pdf --config w2_parse_config.json > w2_output.md
```

## Schema

**No extraction schema is used in this pipeline.** The parse step outputs markdown + bounding boxes directly. However, if you want to *extract* structured fields from the parsed markdown into JSON, here is a recommended Zod schema for a downstream extraction step:

```typescript
import { z } from "zod";
import { extendDate, extendCurrency } from "extend-ai";

const W2ExtractionSchema = z.object({
  // Employee & Employer Identity
  employee_name: z.string().nullable().describe(
    "Employee's full name (Box e/f). Example: ELIZABETH A DARLING"
  ),
  employee_address: z.string().nullable().describe(
    "Employee's mailing address including ZIP (Box e/f). Example: 2001 CAMPUS DRIVE PITTSBURGH, PA 15237"
  ),
  employee_ssn: z.string().nullable().describe(
    "Employee's Social Security Number (Box a). Masked format acceptable. Example: XXX-XX-1234"
  ),
  employer_name: z.string().nullable().describe(
    "Employer's legal name (Box c). Example: UNIVERSITY OF PITTSBURGH"
  ),
  employer_address: z.string().nullable().describe(
    "Employer's address including ZIP (Box c). Example: 4200 FIFTH AVENUE PITTSBURGH, PA 15260"
  ),
  employer_federal_id: z.string().nullable().describe(
    "Employer's Federal ID number (Box b). Masked format acceptable. Example: XX-XXX5591"
  ),
  employer_state_id: z.string().nullable().describe(
    "Employer's state ID number (Box 15). Example: XXXX5369"
  ),

  // Federal Wages & Withholdings
  box_1_wages_tips_compensation: extendCurrency().describe(
    "Wages, tips, other compensation (Box 1). Example: $44,629.35"
  ),
  box_2_federal_income_tax: extendCurrency().describe(
    "Federal income tax withheld (Box 2). Example: $7,631.62"
  ),

  // Social Security
  box_3_ss_wages: extendCurrency().describe(
    "Social Security wages and tips (Box 3). Example: $48,736.35"
  ),
  box_4_ss_tax: extendCurrency().describe(
    "Social Security tax withheld (Box 4). Example: $3,021.65"
  ),

  // Medicare
  box_5_medicare_wages: extendCurrency().describe(
    "Medicare wages and tips (Box 5). Example: $48,736.35"
  ),
  box_6_medicare_tax: extendCurrency().describe(
    "Medicare tax withheld (Box 6). Example: $706.68"
  ),

  // Additional Benefits & Deductions
  box_7_ss_tips: extendCurrency().nullable().describe(
    "Social Security tips (Box 7). Often blank."
  ),
  box_8_allocated_tips: extendCurrency().nullable().describe(
    "Allocated tips (Box 8). Often blank."
  ),
  box_10_dependent_care_benefits: extendCurrency().nullable().describe(
    "Dependent care benefits (Box 10). Example: $1,000.00"
  ),
  box_11_nonqualified_plans: extendCurrency().nullable().describe(
    "Nonqualified plans (Box 11). Often blank."
  ),

  // Box 12: Deferred Compensation & Special Codes
  box_12_entries: z.array(
    z.object({
      code: z.string().nullable().describe(
        "Box 12 code (e.g., 'D' = 401k, 'E' = Student loan interest, 'W' = Pretax dependent care). Example: E or W"
      ),
      amount: extendCurrency().describe(
        "Amount for this Box 12 code. Example: $4,107.00"
      ),
    })
  ).describe(
    "Array of Box 12 entries. Each entry has a code and amount. Example: [{ code: 'E', amount: { amount: 4107, iso_4217_currency_code: 'USD' } }]"
  ),

  // Box 13: Checkboxes
  box_13_statutory_employee: z.boolean().nullable().describe(
    "Is this a statutory employee? (Box 13 checkbox). Example: false"
  ),
  box_13_retirement_plan: z.boolean().nullable().describe(
    "Is employee covered by a retirement plan? (Box 13 checkbox). Example: true"
  ),
  box_13_third_party_sick_pay: z.boolean().nullable().describe(
    "Did employer provide third-party sick pay? (Box 13 checkbox). Example: false"
  ),

  // Box 14: Other
  box_14_other: z.array(
    z.object({
      code: z.string().nullable().describe(
        "Code for Box 14 (e.g., '14H' for hobby loss, '14X' for other). Example: 14H or 14X"
      ),
      amount: extendCurrency().describe(
        "Amount for this Box 14 code. Example: $1,600.00"
      ),
    })
  ).describe(
    "Array of Box 14 'Other' entries. Example: [{ code: '14H', amount: { amount: 1600, iso_4217_currency_code: 'USD' } }]"
  ),

  // State Withholding
  box_15_state: z.string().nullable().describe(
    "State code (Box 15). Two-letter abbreviation. Example: PA"
  ),
  box_16_state_wages: extendCurrency().nullable().describe(
    "State wages, tips, etc. (Box 16). Example: $47,808.35"
  ),
  box_17_state_income_tax: extendCurrency().nullable().describe(
    "State income tax withheld (Box 17). Example:
import { ExtendClient } from "extend-ai";
import fs from "fs";

/**
 * Main entry point: parse a W-2 tax form and output markdown + metadata.
 * 
 * Reads the local file, converts to base64 data URL, then calls Extend parse API
 * with agentic OCR enabled for accurate form field recognition.
 */
async function processW2TaxForm(filePath: string): Promise<void> {
  const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });

  // Convert local file to base64 data URL for API upload.
  // The SDK does not accept Node.js file streams; we must use a data URL or public URL.
  const fileBuffer = fs.readFileSync(filePath);
  const base64 = fileBuffer.toString("base64");
  const dataUrl = `data:application/octet-stream;base64,${base64}`;

  console.log(`[W-2 Parser] Starting parse of: ${filePath}`);
  console.log(`[W-2 Parser] File size: ${fileBuffer.length} bytes`);

  // Create and poll the parse run with agentic OCR + form-specific options.
  const parseRun = await client.parseRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      parseConfig: {
        blockOptions: {
          // Text block parsing with agentic OCR for accurate form field labels and values.
          text: {
            agentic: {
              enabled: true,
            },
            // Detect signature blocks for compliance workflows.
            signatureDetectionEnabled: true,
          },
          // Table agentic mode for Box 12 (codes + amounts) and Box 13 (checkboxes).
          tables: {
            agentic: {
              enabled: true,
            },
            // Preserve table headers across page breaks (defensive for multi-page W-2s).
            tableHeaderContinuationEnabled: true,
          },
          // Capture figures (QR codes, logos, barcodes).
          figures: {
            enabled: true,
          },
        },
        // Page-level chunking: W-2s are typically 1 page; keep form data together.
        chunkingStrategy: {
          type: "page",
          options: {},
        },
      },
    },
  });

  // Check parse result status.
  if (parseRun.status !== "PROCESSED") {
    console.error(
      `[W-2 Parser] Parse failed. Status: ${parseRun.status}`,
      parseRun.error
    );
    process.exit(1);
  }

  console.log(`[W-2 Parser] Parse successful. Status: ${parseRun.status}`);
  console.log(
    `[W-2 Parser] Generated ${parseRun.output.chunks.length} chunk(s).`
  );

  // Output markdown content and metadata for each chunk.
  for (let i = 0; i < parseRun.output.chunks.length; i++) {
    const chunk = parseRun.output.chunks[i];
    console.log(`\n========== CHUNK ${i + 1} ==========`);
    console.log(`Type: ${chunk.type}`);
    console.log(`Metadata:`, JSON.stringify(chunk.metadata, null, 2));
    console.log(`\nMarkdown Content:\n${chunk.content}`);

    // Output block-level details (type, content, bounding boxes).
    if (chunk.blocks && chunk.blocks.length > 0) {
      console.log(`\n--- Block Details (${chunk.blocks.length} blocks) ---`);
      for (let j = 0; j < Math.min(chunk.blocks.length, 5); j++) {
        const block = chunk.blocks[j];
        console.log(`\nBlock ${j + 1}:`);
        console.log(`  Type: ${block.type}`);
        console.log(`  Content (first 100 chars): ${block.content.substring(0, 100)}...`);
        if (block.boundingBox) {
          console.log(
            `  Bounding Box: x=${block.boundingBox.left}, y=${block.boundingBox.top}, ` +
            `width=${block.boundingBox.right - block.boundingBox.left}, ` +
            `height=${block.boundingBox.bottom - block.boundingBox.top}`
          );
        }
        if (block.metadata?.minOcrConfidence) {
          console.log(
            `  OCR Confidence: min=${block.metadata.minOcrConfidence}, ` +
            `avg=${block.metadata.avgOcrConfidence}`
          );
        }
      }
      if (chunk.blocks.length > 5) {
        console.log(`\n... and ${chunk.blocks.length - 5} more blocks (not shown).`);
      }
    }
  }

  // Summary stats.
  const totalBlocks = parseRun.output.chunks.reduce(
    (sum, chunk) => sum + (chunk.blocks?.length || 0),
    0
  );
  console.log(`\n========== SUMMARY ==========`);
  console.log(`Total chunks: ${parseRun.output.chunks.length}`);
  console.log(`Total blocks: ${totalBlocks}`);
  console.log(
    `Parse completed successfully. Markdown is ready for downstream workflows.`
  );
}

// Invoke main function if file path is provided as CLI argument.
const filePath = process.argv[2];
if (!filePath) {
  console.error("Usage: npx ts-node solution.ts <path-to-w2-file>");
  process.exit(1);
}

processW2TaxForm(filePath).catch((err) => {
  console.error("[W-2 Parser] Fatal error:", err);
  process.exit(1);
});
import os
from extend_ai import Extend

client = Extend(token=os.environ["EXTEND_API_KEY"])


def process_w2_tax_form(file_path: str) -> str:
    """
    Parse a W-2 tax form into markdown.
    Handles checkboxes, multiple boxes, handwritten entries, and complex IRS form layouts.
    
    Args:
        file_path: Local path to the W-2 PDF or image
        
    Returns:
        Parsed markdown content
    """
    # Upload file to Extend
    with open(file_path, "rb") as f:
        file = client.files.upload(file=f)
    
    print(f"[W-2 Parse] Starting parse of {file_path}")
    
    # Step 1: Parse with agentic OCR
    # Agentic mode is critical for W-2 forms because:
    # - Handles checkboxes and their states (e.g., "Statutory employee" ☑)
    # - Reads handwritten amounts in wage boxes
    # - Preserves multi-box structure (Box 1, Box 2, etc.) and their labels
    # - Correctly interprets form geometry and field alignment
    parse_run = client.parse_runs.create_and_poll(
        file={"id": file.id},
        config={
            "blockOptions": {
                "text": {
                    "agentic": {
                        "enabled": True,  # Enable agentic OCR for form intelligence
                    },
                },
            },
            "chunkingStrategy": {
                "type": "document",  # Keep entire W-2 as one chunk
            },
        },
    )
    
    print(f"[W-2 Parse] Status: {parse_run.status}")
    
    if parse_run.status != "PROCESSED":
        error_message = parse_run.error.get("message", "Unknown error") if parse_run.error else "Unknown error"
        raise Exception(
            f"Parse failed with status {parse_run.status}: {error_message}"
        )
    
    # Extract markdown from chunks
    markdown_content = "\n\n".join(
        chunk.get("content", "") for chunk in parse_run.output.get("chunks", [])
    )
    
    print(f"[W-2 Parse] Successfully parsed. Output length: {len(markdown_content)} chars")
    print(f"[W-2 Parse] First 500 chars:\n{markdown_content[:500]}...")
    
    return markdown_content


def main():
    import sys
    
    file_path = sys.argv[1] if len(sys.argv) > 1 else "./sample-w2.pdf"
    
    try:
        markdown = process_w2_tax_form(file_path)
        print("\n=== PARSED W-2 MARKDOWN ===\n")
        print(markdown)
    except Exception as error:
        print(f"Error: {str(error)}")
        sys.exit(1)


if __name__ == "__main__":
    main()
// 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 W2TaxFormParser {

  private static final String API_BASE_URL = "https://api.extend.ai";
  private static final String API_KEY = System.getenv("EXTEND_API_KEY");
  private static final HttpClient httpClient = HttpClient.newHttpClient();

  /**
   * Parse a W-2 tax form into markdown.
   * Handles checkboxes, multiple boxes, handwritten entries, and complex IRS form layouts.
   * @param filePath - Local path to the W-2 PDF or image
   * @return Parsed markdown content
   */
  public static String processW2TaxForm(String filePath) throws IOException, InterruptedException {
    // Convert local file to base64 data URL for API compatibility
    byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
    String base64Data = Base64.getEncoder().encodeToString(fileBytes);
    String dataUrl = "data:application/octet-stream;base64," + base64Data;

    System.out.println("[W-2 Parse] Starting parse of " + filePath);

    // Step 1: Parse with agentic OCR
    // Agentic mode is critical for W-2 forms because:
    // - Handles checkboxes and their states (e.g., "Statutory employee" ☑)
    // - Reads handwritten amounts in wage boxes
    // - Preserves multi-box structure (Box 1, Box 2, etc.) and their labels
    // - Correctly interprets form geometry and field alignment
    String requestBody = "{"
        + "\"file\":{\"url\":\"" + escapeJson(dataUrl) + "\"},"
        + "\"config\":{"
        + "\"blockOptions\":{"
        + "\"text\":{"
        + "\"agentic\":{"
        + "\"enabled\":true"
        + "}"
        + "}"
        + "},"
        + "\"chunkingStrategy\":{"
        + "\"type\":\"document\""
        + "}"
        + "}"
        + "}";

    HttpRequest createRequest = HttpRequest.newBuilder()
        .uri(URI.create(API_BASE_URL + "/v1/parseRuns"))
        .header("Authorization", "Bearer " + API_KEY)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(requestBody))
        .build();

    HttpResponse<String> createResponse = httpClient.send(createRequest, HttpResponse.BodyHandlers.ofString());
    String parseRunId = extractFieldFromJson(createResponse.body(), "id");

    if (parseRunId == null || parseRunId.isEmpty()) {
      throw new RuntimeException("Failed to create parse run: " + createResponse.body());
    }

    System.out.println("[W-2 Parse] Created parse run: " + parseRunId);

    // Poll for completion
    String status = "PROCESSING";
    String parseRunJson = "";
    int maxAttempts = 120;
    int attempt = 0;

    while (!status.equals("PROCESSED") && !status.equals("FAILED") && attempt < maxAttempts) {
      Thread.sleep(1000);
      attempt++;

      HttpRequest pollRequest = HttpRequest.newBuilder()
          .uri(URI.create(API_BASE_URL + "/v1/parseRuns/" + parseRunId))
          .header("Authorization", "Bearer " + API_KEY)
          .GET()
          .build();

      HttpResponse<String> pollResponse = httpClient.send(pollRequest, HttpResponse.BodyHandlers.ofString());
      parseRunJson = pollResponse.body();
      status = extractFieldFromJson(parseRunJson, "status");
    }

    System.out.println("[W-2 Parse] Status: " + status);

    if (!status.equals("PROCESSED")) {
      String errorMessage = extractFieldFromJson(parseRunJson, "error");
      throw new RuntimeException("Parse failed with status " + status + ": " + (errorMessage != null ? errorMessage : "Unknown error"));
    }

    // Extract markdown from chunks
    String markdownContent = extractMarkdownFromOutput(parseRunJson);

    System.out.println("[W-2 Parse] Successfully parsed. Output length: " + markdownContent.length() + " chars");
    System.out.println("[W-2 Parse] First 500 chars:\n" + markdownContent.substring(0, Math.min(500, markdownContent.length())) + "...");

    return markdownContent;
  }

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

  private static String extractFieldFromJson(String json, String fieldName) {
    String searchKey = "\"" + fieldName + "\":";
    int startIndex = json.indexOf(searchKey);
    if (startIndex == -1) {
      return null;
    }
    startIndex += searchKey.length();
    while (startIndex < json.length() && Character.isWhitespace(json.charAt(startIndex))) {
      startIndex++;
    }
    if (startIndex >= json.length()) {
      return null;
    }
    if (json.charAt(startIndex) == '"') {
      startIndex++;
      StringBuilder result = new StringBuilder();
      while (startIndex < json.length() && json.charAt(startIndex) != '"') {
        if (json.charAt(startIndex) == '\\' && startIndex + 1 < json.length()) {
          startIndex++;
        }
        result.append(json.charAt(startIndex));
        startIndex++;
      }
      return result.toString();
    }
    return null;
  }

  private static String extractMarkdownFromOutput(String parseRunJson) {
    StringBuilder markdown = new StringBuilder();
    int chunksIndex = parseRunJson.indexOf("\"chunks\":");
    if (chunksIndex == -1) {
      return "";
    }
    int arrayStart = parseRunJson.indexOf("[", chunksIndex);
    int arrayEnd = parseRunJson.indexOf("]", arrayStart);
    if (arrayStart == -1 || arrayEnd == -1) {
      return "";
    }
    String chunksArray = parseRunJson.substring(arrayStart + 1, arrayEnd);
    String[] chunks = chunksArray.split("\\{");
    for (String chunk : chunks) {
      String content = extractFieldFromJson("{" + chunk, "content");
      if (content != null && !content.isEmpty()) {
        if (markdown.length() > 0) {
          markdown.append("\n\n");
        }
        markdown.append(content);
      }
    }
    return markdown.toString();
  }

  public static void main(String[] args) {
    String filePath = args.length > 0 ? args[0] : "./sample-w2.pdf";

    try {
      String markdown = processW2TaxForm(filePath);
      System.out.println("\n=== PARSED W-2 MARKDOWN ===\n");
      System.out.println(markdown);
    } catch (Exception e) {
      System.err.println("Error: " + e.getMessage());
      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"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

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

// ParseRunResponse represents the response from the parse runs API
type ParseRunResponse struct {
	ID     string `json:"id"`
	Status string `json:"status"`
	Error  *struct {
		Message string `json:"message"`
	} `json:"error"`
	Output *struct {
		Chunks []struct {
			Content string `json:"content"`
		} `json:"chunks"`
	} `json:"output"`
}

// ProcessW2TaxForm parses a W-2 tax form into markdown.
// Handles checkboxes, multiple boxes, handwritten entries, and complex IRS form layouts.
func ProcessW2TaxForm(filePath string) (string, error) {
	// Read file and convert to base64 data URL
	fileBuffer, err := os.ReadFile(filePath)
	if err != nil {
		return "", fmt.Errorf("failed to read file: %w", err)
	}

	base64Data := base64.StdEncoding.EncodeToString(fileBuffer)
	dataURL := fmt.Sprintf("data:application/octet-stream;base64,%s", base64Data)

	fmt.Printf("[W-2 Parse] Starting parse of %s\n", filePath)

	// Step 1: Parse with agentic OCR
	// Agentic mode is critical for W-2 forms because:
	// - Handles checkboxes and their states (e.g., "Statutory employee" ☑)
	// - Reads handwritten amounts in wage boxes
	// - Preserves multi-box structure (Box 1, Box 2, etc.) and their labels
	// - Correctly interprets form geometry and field alignment

	requestBody := map[string]interface{}{
		"file": map[string]string{
			"url": dataURL,
		},
		"config": map[string]interface{}{
			"blockOptions": map[string]interface{}{
				"text": map[string]interface{}{
					"agentic": map[string]bool{
						"enabled": true, // Enable agentic OCR for form intelligence
					},
				},
			},
			"chunkingStrategy": map[string]string{
				"type": "document", // Keep entire W-2 as one chunk
			},
		},
	}

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

	// Create parse run
	req, err := http.NewRequest("POST", extendAPIBase+"/parseRuns", bytes.NewReader(jsonBody))
	if err != nil {
		return "", fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("EXTEND_API_KEY")))
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("failed to create parse run: %w", err)
	}
	defer resp.Body.Close()

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

	// Poll for completion
	for {
		if parseRun.Status == "PROCESSED" || parseRun.Status == "FAILED" {
			break
		}

		time.Sleep(2 * time.Second)

		pollReq, err := http.NewRequest("GET", extendAPIBase+"/parseRuns/"+parseRun.ID, nil)
		if err != nil {
			return "", fmt.Errorf("failed to create poll request: %w", err)
		}

		pollReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("EXTEND_API_KEY")))

		pollResp, err := client.Do(pollReq)
		if err != nil {
			return "", fmt.Errorf("failed to poll parse run: %w", err)
		}

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

	fmt.Printf("[W-2 Parse] Status: %s\n", parseRun.Status)

	if parseRun.Status != "PROCESSED" {
		errMsg := "Unknown error"
		if parseRun.Error != nil {
			errMsg = parseRun.Error.Message
		}
		return "", fmt.Errorf("parse failed with status %s: %s", parseRun.Status, errMsg)
	}

	// Extract markdown from chunks
	var markdownContent string
	if parseRun.Output != nil {
		for i, chunk := range parseRun.Output.Chunks {
			if i > 0 {
				markdownContent += "\n\n"
			}
			markdownContent += chunk.Content
		}
	}

	fmt.Printf("[W-2 Parse] Successfully parsed. Output length: %d chars\n", len(markdownContent))
	if len(markdownContent) > 500 {
		fmt.Printf("[W-2 Parse] First 500 chars:\n%s...\n", markdownContent[:500])
	} else {
		fmt.Printf("[W-2 Parse] Content:\n%s\n", markdownContent)
	}

	return markdownContent, nil
}

func main() {
	filePath := "./sample-w2.pdf"
	if len(os.Args) > 1 {
		filePath = os.Args[1]
	}

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

	fmt.Println("\n=== PARSED W-2 MARKDOWN ===\n")
	fmt.Println(markdown)
}
// Deploy the "W-2 Tax Form" 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/w-2-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: w-2-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, "w-2-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": "W-2 Tax Form Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              },
              "signatureDetectionEnabled": true
            },
            "tables": {
              "agentic": {
                "enabled": true
              },
              "tableHeaderContinuationEnabled": true
            },
            "figures": {
              "enabled": true
            }
          },
          "chunkingStrategy": {
            "type": "page",
            "options": {}
          }
        }
      }
    }
  ]
};

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 extend_ai import Extend

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 / "w-2-tax-form.json"

state: dict = {}
if STATE_FILE.exists():
    with open(STATE_FILE, "r") as f:
        state = json.load(f)

def save_state():
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2)

client = Extend(token=API_KEY)

WORKFLOW = {
    "name": "W-2 Tax Form Processing Pipeline",
    "steps": [
        {
            "name": "startTrigger1",
            "type": "TRIGGER",
            "next": [
                {
                    "step": "parse1"
                }
            ]
        },
        {
            "name": "parse1",
            "type": "PARSE",
            "config": {
                "parseConfig": {
                    "blockOptions": {
                        "text": {
                            "agentic": {
                                "enabled": True
                            }
                        }
                    },
                    "chunkingStrategy": {
                        "type": "document"
                    }
                }
            }
        }
    ]
}

def main():
    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:
        existing_id = None
        try:
            workflows_list = client.workflows.list(name=WORKFLOW["name"])
            items = workflows_list.data if hasattr(workflows_list, "data") else (workflows_list.items if hasattr(workflows_list, "items") else [])
            for item in items:
                if item.get("name") == WORKFLOW["name"]:
                    existing_id = item.get("id")
                    break
            if 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 hasattr(created, "id") else (created.workflow.id if hasattr(created, "workflow") else None)
            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})")

    try:
        client.workflows.create_version(id=state["workflowId"])
    except Exception:
        pass

    print("\nDone. Run documents through it with:")
    print(f"  POST https://api.extend.ai/workflow_runs  {{ \"workflow\": {{ \"id\": \"{state['workflowId']}\" }}, \"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.
// Call with: export EXTEND_API_KEY=sk_... && java Provision.java

import java.io.*;
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.*;

public class Provision {
  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 Path STATE_DIR = Paths.get(System.getProperty("user.dir"), ".extend");
  private static final Path STATE_FILE = STATE_DIR.resolve("w-2-tax-form.json");

  static {
    if (API_KEY == null || API_KEY.isEmpty()) {
      System.err.println("Set EXTEND_API_KEY first.");
      System.exit(1);
    }
  }

  static class State {
    String workflowId;
  }

  private static State state = new State();
  private static final HttpClient httpClient = HttpClient.newHttpClient();

  static {
    if (Files.exists(STATE_FILE)) {
      try {
        String content = Files.readString(STATE_FILE);
        state = parseJson(content, State.class);
      } catch (IOException e) {
        state = new State();
      }
    }
  }

  private static void saveState() throws IOException {
    Files.createDirectories(STATE_DIR);
    String json = toJson(state);
    Files.writeString(STATE_FILE, json);
  }

  private static Map<String, Object> api(String method, String pathName, Object body) throws Exception {
    HttpRequest.Builder builder = HttpRequest.newBuilder()
        .uri(URI.create(API + pathName))
        .method(method, body != null
            ? HttpRequest.BodyPublishers.ofString(toJson(body))
            : HttpRequest.BodyPublishers.noBody())
        .header("Authorization", "Bearer " + API_KEY)
        .header("x-extend-api-version", VERSION);

    if (body != null) {
      builder.header("Content-Type", "application/json");
    }

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

    Map<String, Object> data = new HashMap<>();
    if (!response.body().isEmpty()) {
      data = parseJson(response.body(), Map.class);
    }

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

    return data;
  }

  private static String toJson(Object obj) {
    if (obj instanceof String) return "\"" + ((String) obj).replace("\"", "\\\"") + "\"";
    if (obj instanceof Number) return obj.toString();
    if (obj instanceof Boolean) return obj.toString();
    if (obj instanceof Map) {
      Map<String, Object> map = (Map<String, Object>) obj;
      StringBuilder sb = new StringBuilder("{");
      boolean first = true;
      for (Map.Entry<String, Object> e : map.entrySet()) {
        if (!first) sb.append(",");
        sb.append("\"").append(e.getKey()).append("\":").append(toJson(e.getValue()));
        first = false;
      }
      sb.append("}");
      return sb.toString();
    }
    if (obj instanceof List) {
      List<?> list = (List<?>) obj;
      StringBuilder sb = new StringBuilder("[");
      for (int i = 0; i < list.size(); i++) {
        if (i > 0) sb.append(",");
        sb.append(toJson(list.get(i)));
      }
      sb.append("]");
      return sb.toString();
    }
    if (obj instanceof State) {
      State s = (State) obj;
      Map<String, Object> map = new LinkedHashMap<>();
      if (s.workflowId != null) map.put("workflowId", s.workflowId);
      return toJson(map);
    }
    return "null";
  }

  @SuppressWarnings("unchecked")
  private static <T> T parseJson(String json, Class<T> clazz) {
    json = json.trim();
    if (clazz == Map.class) {
      return (T) parseJsonObject(json);
    }
    if (clazz == State.class) {
      Map<String, Object> map = parseJsonObject(json);
      State s = new State();
      s.workflowId = (String) map.get("workflowId");
      return (T) s;
    }
    return null;
  }

  private static Map<String, Object> parseJsonObject(String json) {
    Map<String, Object> result = new LinkedHashMap<>();
    json = json.trim();
    if (!json.startsWith("{") || !json.endsWith("}")) return result;
    json = json.substring(1, json.length() - 1).trim();
    if (json.isEmpty()) return result;

    int depth = 0;
    StringBuilder key = new StringBuilder();
    StringBuilder value = new StringBuilder();
    boolean inString = false;
    boolean readingKey = true;

    for (int i = 0; i < json.length(); i++) {
      char c = json.charAt(i);
      if (c == '"' && (i == 0 || json.charAt(i - 1) != '\\')) {
        inString = !inString;
      }
      if (!inString) {
        if (c == '{' || c == '[') depth++;
        else if (c == '}' || c == ']') depth--;
        else if (c == ':' && depth == 0 && readingKey) {
          readingKey = false;
          continue;
        } else if (c == ',' && depth == 0) {
          String k = key.toString().trim().replaceAll("^\"|\"$", "");
          String v = value.toString().trim();
          result.put(k, parseJsonValue(v));
          key = new StringBuilder();
          value = new StringBuilder();
          readingKey = true;
          continue;
        }
      }
      if (readingKey) key.append(c);
      else value.append(c);
    }
    if (key.length() > 0) {
      String k = key.toString().trim().replaceAll("^\"|\"$", "");
      String v = value.toString().trim();
      result.put(k, parseJsonValue(v));
    }
    return result;
  }

  private static Object parseJsonValue(String v) {
    v = v.trim();
    if (v.startsWith("\"") && v.endsWith("\"")) {
      return v.substring(1, v.length() - 1).replace("\\\"", "\"");
    }
    if (v.equals("true")) return true;
    if (v.equals("false")) return false;
    if (v.equals("null")) return null;
    if (v.startsWith("{")) return parseJsonObject(v);
    if (v.startsWith("[")) {
      List<Object> list = new ArrayList<>();
      v = v.substring(1, v.length() - 1).trim();
      if (!v.isEmpty()) {
        int depth = 0;
        StringBuilder item = new StringBuilder();
        boolean inString = false;
        for (char c : v.toCharArray()) {
          if (c == '"' && (item.length() == 0 || item.charAt(item.length() - 1) != '\\')) {
            inString = !inString;
          }
          if (!inString && (c == '{' || c == '[')) depth++;
          else if (!inString && (c == '}' || c == ']')) depth--;
          else if (!inString && c == ',' && depth == 0) {
            list.add(parseJsonValue(item.toString().trim()));
            item = new StringBuilder();
            continue;
          }
          item.append(c);
        }
        if (item.length() > 0) list.add(parseJsonValue(item.toString().trim()));
      }
      return list;
    }
    try {
      if (v.contains(".")) return Double.parseDouble(v);
      return Long.parseLong(v);
    } catch (NumberFormatException e) {
      return v;
    }
  }

  private static Map<String, Object> buildWorkflow() {
    Map<String, Object> workflow = new LinkedHashMap<>();
    workflow.put("name", "W-2 Tax Form Processing Pipeline");

    List<Map<String, Object>> steps = new ArrayList<>();

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

    Map<String, Object> parse = new LinkedHashMap<>();
    parse.put("name", "parse1");
    parse.put("type", "PARSE");
    Map<String, Object> config = new LinkedHashMap<>();
    Map<String, Object> parseConfig = new LinkedHashMap<>();
    Map<String, Object> blockOptions = new LinkedHashMap<>();
    Map<String, Object> text = new LinkedHashMap<>();
    Map<String, Object> agentic = new LinkedHashMap<>();
    agentic.put("enabled", true);
    text.put("agentic", agentic);
    blockOptions.put("text", text);
    parseConfig.put("blockOptions", blockOptions);
    Map<String, Object> chunkingStrategy = new LinkedHashMap<>();
    chunkingStrategy.put("type", "document");
    parseConfig.put("chunkingStrategy", chunkingStrategy);
    config.put("parseConfig", parseConfig);
    parse.put("config", config);
    steps.add(parse);

    workflow.put("steps", steps);
    return workflow;
  }

  public static void main(String[] args) throws Exception {
    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> list = api("GET", "/workflows?name=" + encodedName, null);
        List<Map<String, Object>> items = (List<Map<String, Object>>) list.getOrDefault("data",
            list.getOrDefault("items", new ArrayList<>()));
        Map<String, Object> existing = null;
        for (Map<String, Object> item : items) {
          if (workflowName.equals(item.get("name"))) {
            existing = item;
            break;
          }
        }
        if (existing != null && existing.get("id") != null) {
          state.workflowId = (String) existing.get("id");
          saveState();
          System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + state.workflowId
              + ") — updating steps");
          Map<String, Object> updateBody = new LinkedHashMap<>();
          updateBody.put("steps", workflow.get("steps"));
          api("POST", "/workflows/" + state.workflowId, updateBody);
        }
      } 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) {
          Map<String, Object> workflowObj = (Map<String, Object>) created.get("workflow");
          if (workflowObj != null) {
            wfId = (String) workflowObj.get("id");
          }
        }
        if (wfId == null) {
          throw new Exception("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.");
  }
}
// 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/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
)

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 WorkflowStep struct {
	Name   string        `json:"name"`
	Type   string        `json:"type"`
	Next   []interface{} `json:"next,omitempty"`
	Config interface{}   `json:"config,omitempty"`
}

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

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

type WorkflowCreateResponse struct {
	ID       string                 `json:"id,omitempty"`
	Workflow map[string]interface{} `json:"workflow,omitempty"`
}

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

	stateDir = filepath.Join(".", ".extend")
	stateFile = filepath.Join(stateDir, "w-2-tax-form.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", "Bearer "+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()

	respData := make(map[string]interface{})
	respBody, _ := io.ReadAll(resp.Body)
	json.Unmarshal(respBody, &respData)

	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 respData, nil
}

func main() {
	workflow := Workflow{
		Name: "W-2 Tax Form Processing Pipeline",
		Steps: []WorkflowStep{
			{
				Name: "startTrigger1",
				Type: "TRIGGER",
				Next: []interface{}{
					map[string]interface{}{
						"step": "parse1",
					},
				},
			},
			{
				Name: "parse1",
				Type: "PARSE",
				Config: map[string]interface{}{
					"parseConfig": map[string]interface{}{
						"blockOptions": map[string]interface{}{
							"text": map[string]interface{}{
								"agentic": map[string]interface{}{
									"enabled": true,
								},
							},
						},
						"chunkingStrategy": map[string]interface{}{
							"type": "document",
						},
					},
				},
			},
		},
	}

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

	state := loadState()

	if state.WorkflowID != "" {
		fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
		_, err := apiCall("POST", "/workflows/"+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)
		listResp, err := apiCall("GET", "/workflows?name="+query, nil)
		if err == nil {
			var items []map[string]interface{}
			if data, ok := listResp["data"].([]interface{}); ok {
				for _, item := range data {
					if m, ok := item.(map[string]interface{}); ok {
						items = append(items, m)
					}
				}
			} else if itemsRaw, ok := listResp["items"].([]interface{}); ok {
				for _, item := range itemsRaw {
					if m, ok := item.(map[string]interface{}); ok {
						items = append(items, m)
					}
				}
			}

			for _, item := range items {
				if name, ok := item["name"].(string); ok && name == workflow.Name {
					if id, ok := item["id"].(string); ok {
						state.WorkflowID = id
						saveState(state)
						fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflow.Name, id)
						_, err := apiCall("POST", "/workflows/"+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)
			}

			var wfID string
			if id, ok := created["id"].(string); ok {
				wfID = id
			} else if wfObj, ok := created["workflow"].(map[string]interface{}); ok {
				if id, ok := wfObj["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", "/workflows/"+state.WorkflowID+"/versions", 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)

Probably not. For W-2s, agentic mode adds latency without much benefit—the form structure is rigid and predictable. Disable it (`"enabled": false`) to speed up processing. Agentic is better for unstructured documents or handwritten content where AI reasoning helps.
Tags
PayrollTaxComplianceEmployee Records
About this template

This W-2 Tax Form parser template turns all form content into clean markdown, including employee wage and tax information including federal, state, and local withholdings, Social Security and Medicare taxes, and other compensation details. It handles structured financial data with checkboxes and multiple sections across standard IRS form layouts.

Document formats
  • PDF
Requirements
  • Checkboxes & Strikethroughs
  • Complex layouts
  • Long tables

Relevant templates for Financial & Banking

  1. 01
    Driver's License ExtractorParse → Extract
    Extracts personal identification and licensing data from driver license documents.
    PDFImages & Scanswww.extend.ai/templates/driver-license-template
  2. 02
    Receipt ExtractorParse → Extract
    Extracts itemized sales, pricing, GST tax, and payment details from retail receipts.
    PDFImages & Scanswww.extend.ai/templates/receipt-parse-extract
  3. 03
    Bank Statement ExtractorParse → Extract
    Extracts account summaries, balances, deposits, NSF flags, and transaction details from bank statements.
    PDFImages & Scanswww.extend.ai/templates/bank-statement
  4. 04
    Vendor Invoice ExtractorParse → Extract
    Extracts charges, billing details, line items, totals, and due dates vendor invoices.
    PDFImages & Scanswww.extend.ai/templates/vendor-invoice
  5. 05
    Pay Stub ExtractorParse → Extract
    Extracts employee earnings, deductions, taxes, and net pay from pay stubs.
    PDFwww.extend.ai/templates/pay-stub
  6. 06
    Check ExtractorParse → Extract
    Extracts check details including payee, amount, date, and bank routing information.
    PDFImages & Scanswww.extend.ai/templates/check
  7. 07
    Wire Transfer Instructions ExtractorParse → Extract
    Extracts wire transfer procedures, contact info, and operational hours from banking guides.
    PDFWord / DOCXwww.extend.ai/templates/wire-transfer-instructions
  8. 08
    Onboarding Package ExtractorParse → Extract
    Extracts account summaries, transaction details, and balances from bank statements.
    PDFImages & Scanswww.extend.ai/templates/personal-bank-statement