1120 Extractor · Extend Templates

Back to library

Financial & BankingParse → Extract

1120 Extractor

Extracts S Corporation income tax return data and financial information from Form 1120-S.

Send to OpenAI Codex Send to Claude Code Ship it with Extend

Live pipeline

a real document, processed end to end · view only

Run your document ( free) Play flow

Source document

27 Expand document 775549278-Akash-Inc-1120-S-2023.pdf

See Parse output

ParseParse form 1120-s to markdownview only · click for config

Visualize document

See Extract output

ExtractExtract form 1120-s fieldsview only · click for config

Visualize document

Download

Need help? Book 15 min with the team

Step-by-step

Form 1120-S is the IRS tax return form filed annually by S corporations to report corporate income, deductions, tax liability, and shareholder allocation information for federal income tax purposes. This template takes in Form 1120-S: U.S. Income Tax Return for S Corporations and outputs markdown (.md) preserving the form's structure and textual content, and JSON (.json) with extracted corporate identity, financial totals, and shareholder data fields per the extraction schema by using Extend's Parse, Extract primitives.

Input

Form 1120-S: U.S. Income Tax Return for S Corporations

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.pcxShow all (35)

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

blockOptions.text.agentic.enabledtruechanged

chunkingStrategy.type"document"

engine"parse_performance"

OutputMarkdown chunked by page or section, plus typed blocks (text, table, figure) with bounding boxes

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

Step 2

Extract

Pulls a defined set of fields from the document and returns them as structured JSON matching a schema.

InputOutput of the Parse step

Config

schemacustom schema — 12 fieldschanged

advancedOptions.advancedMultimodalEnabledtruechanged

advancedOptions.reviewAgent.enabledtruechanged

baseProcessor"extraction_performance"

Show all (4)

OutputJSON shaped to the extraction schema, with per-field confidence scores and citations grounding each value to its source location

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

Example code

ProvisionSnippetWorkflow JSONSkill.md

TypeScriptPythonJavaGo

{
  "name": "Form 1120-S Processing Pipeline",
  "steps": [\
    {\
      "name": "startTrigger1",\
      "type": "TRIGGER",\
      "next": [\
        {\
          "step": "parse1"\
        }\
      ]\
    },\
    {\
      "name": "parse1",\
      "type": "PARSE",\
      "config": {\
        "parseConfig": {\
          "blockOptions": {\
            "text": {\
              "agentic": {\
                "enabled": true\
              }\
            }\
          },\
          "chunkingStrategy": {\
            "type": "document"\
          }\
        }\
      },\
      "next": [\
        {\
          "step": "extraction2"\
        }\
      ]\
    },\
    {\
      "name": "extraction2",\
      "type": "EXTRACT",\
      "config": {\
        "extractorConfig": {\
          "schema": {\
            "type": "object",\
            "properties": {\
              "ein": {\
                "type": [\
                  "string",\
                  "null"\
                ],\
                "description": "Employer Identification Number"\
              },\
              "tax_year": {\
                "type": [\
                  "string",\
                  "null"\
                ],\
                "description": "Calendar year or tax year period"\
              },\
              "form_name": {\
                "type": [\
                  "string",\
                  "null"\
                ],\
                "description": "Form identifier and title"\
              },\
              "gross_profit": {\
                "type": [\
                  "string",\
                  "null"\
                ],\
                "description": "Gross profit after cost of goods sold"\
              },\
              "total_assets": {\
                "type": [\
                  "string",\
                  "null"\
                ],\
                "description": "Total assets at end of tax year"\
              },\
              "total_income": {\
                "type": [\
                  "string",\
                  "null"\
                ],\
                "description": "Total income or loss for tax year"\
              },\
              "gross_receipts": {\
                "type": [\
                  "string",\
                  "null"\
                ],\
                "description": "Gross receipts or sales before returns and allowances"\
              },\
              "business_address": {\
                "type": [\
                  "string",\
                  "null"\
                ],\
                "description": "Street address, city, state, and ZIP code"\
              },\
              "corporation_name": {\
                "type": [\
                  "string",\
                  "null"\
                ],\
                "description": "Legal name of the S corporation"\
              },\
              "number_of_shareholders": {\
                "type": [\
                  "string",\
                  "null"\
                ],\
                "description": "Number of shareholders during any part of tax year"\
              },\
              "return_type_indicators": {\
                "type": [\
                  "string",\
                  "null"\
                ],\
                "description": "Indicates if final, amended, name change, address change, or election termination"\
              },\
              "s_election_effective_date": {\
                "type": [\
                  "string",\
                  "null"\
                ],\
                "description": "Date S corporation election became effective"\
              }\
            }\
          },\
          "baseProcessor": "extraction_performance",\
          "advancedOptions": {\
            "reviewAgent": {\
              "enabled": true\
            },\
            "advancedMultimodalEnabled": true\
          }\
        }\
      }\
    }\
  ]
}
# Form 1120-S Processing — Extend AI Skill

## What this pipeline does

Automatically extract and structure key data from IRS Form 1120-S (U.S. Income Tax Return for S Corporations) including corporation identity, tax year, financial totals, shareholder count, and return type indicators. The pipeline parses the form to markdown using agentic OCR (handles scans, handwriting, and complex layouts), then extracts 11 core fields into JSON with review-agent validation to catch extraction errors before downstream processing.

## When to use this

- **Tax accounting workflows**: Bulk-process Form 1120-S filings to populate corporate tax databases or accounting software
- **Due diligence**: Extract corporate identity and asset data during M&A, lending, or regulatory reviews
- **Compliance automation**: Flag amended returns, name/address changes, or election terminations at extraction time
- **Hybrid human-AI review**: Use the review-agent flag to surface low-confidence extractions for CPA validation before filing
- **Multi-form batches**: Chain this pipeline with classification to identify 1120-S forms within mixed document bundles

## Processor pipeline

### Step 1: Parse (agentic OCR to markdown)
**Processor**: `parse_performance` with `agentic_ocr` mode
**Purpose**: Convert the 1120-S form (PDF, scan, or image) into searchable markdown while preserving layout context
**Key config**:
- `blockOptions.text.agentic.enabled: true` — uses Claude vision + OCR to handle:
  - Scanned or faxed forms (not just clean PDFs)
  - Handwritten entries in boxes
  - Complex multi-line fields and tables
- `chunkingStrategy.type: "document"` — treats the entire form as one logical document (no page breaks in extraction)

**Why this config**: Form 1120-S is a standardized IRS template with consistent field positions but often appears as scans or faxes with variable quality. Agentic OCR ensures accurate read-through even if boxes are hand-filled, stamped, or contain correction marks. Document-level chunking preserves the relationship between header fields (name, EIN) and financial summary rows.

### Step 2: Extract (structured fields to JSON)
**Processor**: `extraction_performance` with Zod schema
**Purpose**: Pull 11 key fields (corporation name, EIN, tax year, financial totals, shareholder count, return status) into strongly-typed JSON
**Key config**:
- `baseProcessor: "extraction_performance"` — high-accuracy model, optimal for tax forms (vs. light mode which trades accuracy for speed)
- `advancedOptions.reviewAgent.enabled: true` — Claude review loop flags extractions where confidence is low; surfaces ambiguities for human triage
- `advancedMultimodalEnabled: true` — uses both text and visual context (bounding boxes, field positions) to disambiguate overlapping or smudged entries
- Schema: 11 fields, all nullable strings (tax forms may have blank boxes; null preserves that signal for downstream audit)

**Why this config**: Tax forms are high-stakes; extraction errors flow into IRS databases or downstream accounting systems. Review-agent validation is worth the latency. Multimodal context (visual position + text) helps distinguish, e.g., "line 1a gross profit" from "line 1c total income" when OCR text is ambiguous. Performance mode is standard for regulated filings.

## TypeScript implementation

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

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

// Zod schema matching the Form 1120-S extraction contract
const Form1120SSchema = z.object({
form_name: z.string().nullable().describe(
    "Form identifier and title (e.g., 'Form 1120-S U.S. Income Tax Return for S Corporations')"
),
tax_year: z.string().nullable().describe(
    "Calendar year or tax year period (e.g., '2023' or '2023-2024')"
),
corporation_name: z.string().nullable().describe(
    "Legal name of the S corporation as registered with the IRS"
),
ein: z.string().nullable().describe(
    "Employer Identification Number (format: XX-XXXXXXX)"
),
business_address: z.string().nullable().describe(
    "Street address, city, state, and ZIP code of principal business location"
),
s_election_effective_date: z.string().nullable().describe(
    "Date S corporation election became effective (ISO format: YYYY-MM-DD)"
),
total_assets: z.string().nullable().describe(
    "Total assets at end of tax year, including cash equivalents and property (with $ and commas if present)"
),
gross_receipts: z.string().nullable().describe(
    "Gross receipts or sales before returns and allowances, found in Part I income section"
),
gross_profit: z.string().nullable().describe(
    "Gross profit after cost of goods sold, calculated or stated in Part I"
),
total_income: z.string().nullable().describe(
    "Total income or loss for tax year, typically the bottom line of Part I or Part II"
),
number_of_shareholders: z.string().nullable().describe(
    "Number of shareholders during any part of tax year, stated in Schedule K or header section"
),
return_type_indicators: z.string().nullable().describe(
    "Checkboxes or flags indicating if return is final, amended, name change, address change, or election termination"
),
});

/**
* Process Form 1120-S: parse to markdown, then extract structured fields.
* Accepts a local file path, converts to data URL, uploads, and runs both steps.
* Returns parsed markdown and extracted JSON with review-agent validation.
*/
export async function processForm1120S(filePath: string) {
console.log(`Processing Form 1120-S from: ${filePath}`);

// Convert local file to data URL (base64)
const fileBuffer = fs.readFileSync(filePath);
const base64 = fileBuffer.toString("base64");
const dataUrl = `data:application/octet-stream;base64,${base64}`;

// Step 1: Parse (agentic OCR to markdown)
console.log("\n[Step 1] Parsing form to markdown...");
const parseRun = await client.parseRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      blockOptions: {
        text: {
          agentic: {
            enabled: true,
          },
        },
      },
      chunkingStrategy: {
        type: "document",
      },
    },
});

let markdown = "";
if (parseRun.status === "PROCESSED") {
    markdown = parseRun.output.chunks.map((chunk) => chunk.content).join("\n\n");
    console.log(`✓ Parsed successfully (${parseRun.output.chunks.length} chunks)`);
} else {
    throw new Error(
      `Parse failed with status: ${parseRun.status} — ${parseRun.error?.message || "unknown error"}`
    );
}

// Step 2: Extract structured fields with review-agent validation
console.log("\n[Step 2] Extracting structured fields...");
const extractRun = await client.extractRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      schema: Form1120SSchema,
      baseProcessor: "extraction_performance",
      advancedOptions: {
        reviewAgent: {
          enabled: true,
        },
        advancedMultimodalEnabled: true,
      },
    },
});

let extracted = {};
if (extractRun.status === "PROCESSED") {
    extracted = extractRun.output.value;
    console.log("✓ Extraction completed with review-agent validation");
} else {
    throw new Error(
      `Extraction failed with status: ${extractRun.status} — ${extractRun.error?.message || "unknown error"}`
    );
}

// Return both outputs for downstream use (RAG, compliance checks, database insert, etc.)
return {
    status: "success",
    filePath,
    parsed: {
      markdown,
      chunkCount: parseRun.output.chunks.length,
    },
    extracted,
    extractRunId: extractRun.id,
    parseRunId: parseRun.id,
};
}

/**
* Main entry point: accept file path from CLI or test harness.
*/
async function main(filePath: string) {
const result = await processForm1120S(filePath);
console.log("\n=== RESULT ===");
console.log(JSON.stringify(result, null, 2));
return result;
}

// Auto-invoke if called directly
const args = process.argv.slice(2);
if (args.length > 0) {
main(args[0]).catch((err) => {
    console.error("Error:", err.message);
    process.exit(1);
});
}
```

## CLI equivalent

```bash
# Step 1: Parse to markdown
extend parse form1120s.pdf --config '{
"blockOptions": {
    "text": {
      "agentic": {
        "enabled": true
      }
    }
},
"chunkingStrategy": {
    "type": "document"
}
}'

# Step 2: Extract structured fields (using saved schema)
extend extract form1120s.pdf --schema form1120s-schema.json \
  --base-processor extraction_performance \
  --advanced-options '{
    "reviewAgent": {
      "enabled": true
    },
    "advancedMultimodalEnabled": true
}'
```

**Save schema to `form1120s-schema.json`** for reuse:
```bash
cat > form1120s-schema.json <<'EOF'
{
"type": "object",
"properties": {
    "form_name": { "type": ["string", "null"], "description": "Form identifier and title" },
    "tax_year": { "type": ["string", "null"], "description": "Calendar year or tax year period" },
    "corporation_name": { "type": ["string", "null"], "description": "Legal name of the S corporation" },
    "ein": { "type": ["string", "null"], "description": "Employer Identification Number" },
    "business_address": { "type": ["string", "null"], "description": "Street address, city, state, ZIP" },
    "s_election_effective_date": { "type": ["string", "null"], "description": "Date S election became effective" },
    "total_assets": { "type": ["string", "null"], "description": "Total assets at end of tax year" },
    "gross_receipts": { "type": ["string", "null"], "description": "Gross receipts or sales" },
    "gross_profit": { "type": ["string", "null"], "description": "Gross profit after COGS" },
    "total_income": { "type": ["string", "null"], "description": "Total income or loss for tax year" },
    "number_of_shareholders": { "type": ["string", "null"], "description": "Number of shareholders" },
    "return_type_indicators": { "type": ["string", "null"], "description": "Final, amended, name/address change, or election termination flags" }
}
}
EOF

extend extract form1120s.pdf --schema form1120s-schema.json \
  --base-processor extraction_performance \
  --review-agent
```

## Schema

```json
{
"type": "object",
"properties": {
    "form_name": {
      "type": ["string", "null"],
      "description": "Form identifier and title (e.g., 'Form 1120-S U.S. Income Tax Return for S Corporations'). Extracted from the top of the form header."
    },
    "tax_year": {
      "type": ["string", "null"],
      "description": "Calendar year or tax year period, typically a 4-digit year (e.g., '2023') or range (e.g., '2023-2024'). Found in the upper-right header box. Critical for routing to correct tax period in downstream systems."
    },
    "corporation_name": {
      "type": ["string", "null"],
      "description": "Legal name of the S corporation as it appears on the return, exactly as registered with the IRS. Located in Part I, line 1. Must match EIN records for compliance verification."
    },
    "ein": {
      "type": ["string", "null"],
      "description": "Employer Identification Number in format XX-XXXXXXX (e.g., '12-3456789'). Found in Part I header next to corporation name. Primary key for database lookups and IRS matching."
    },
    "business_address": {
      "type": ["string", "null"],
      "description": "Street address, city, state, and ZIP code of the principal business location (not mailing address unless they are the same). Located in Part I below corporation name. Used for jurisdiction verification and notice routing."
    },
    "s_election_effective_date": {
      "type": ["string", "null"],
      "description": "Date the S corporation election became effective (ISO format: YYYY-MM-DD, e.g., '2023-01-01'). Found in Schedule B or Part I header. Determines whether corp qualifies as S for the tax year and validates election sequence."
    },
    "total_assets": {
      "type": ["string", "null"],
      "description": "Total assets at end of tax year including cash, receivables, inventory, property, and intangibles (formatted with $ and commas if present, e.g., '$1,234,567.89'). Located in Part I, balance sheet section. Flags size-based compliance thresholds (e.g., Form 3115 requirements at $10M+)."
    },
    "gross_receipts": {
      "type": ["string", "null"],
      "description": "Gross receipts or sales before returns and allowances (formatted with $ and commas if present). Found in Part I, line 1a or 1b. Essential for estimating corporate revenue and determining audit risk scores."
    },
    "gross_profit": {
      "type": ["string", "null"],
      "description": "Gross profit after cost of goods sold (formatted with $ and commas if present). Found in Part I, line 1d or calculated as gross_receipts minus cost_of_goods_sold. Used to validate margin reasonableness and detect data-entry errors."
    },
    "total_income": {
      "type": ["string", "null"],
      "description": "Total income or loss for the tax year (formatted with $ and commas if present), typically the sum of all income and deduction lines. Found at the bottom of Part I or in Part II, line 22. The primary tax-reporting figure; null if loss or zero."
    },
    "number_of_shareholders": {
      "type": ["string", "null"],
      "description": "Number of shareholders during any part of the tax year (as a count, e.g., '5' or '25'). Located in Schedule K, Part I or in the header section. Validates S election eligibility (max 100 shareholders as of 2024) and identifies closely-held vs. broad-base structure."
    },
    "return_type_indicators": {
      "type": ["string", "null"],
      "description": "Concatenated string of return-type flags: 'Final Return', 'Amended Return', 'Name Change', 'Address Change', 'Election Termination', or combination thereof. Found in checkboxes at top of form. Triggers special handling: amended returns require restating prior-year items; termination signals loss of S status; name/address changes update corporate records."
    }
}
}
```

**Schema design notes:**
- All fields are nullable to preserve IRS form logic: a blank box signals "not applicable" or "zero," and `null` distinguishes that from a failed extraction.
- Monetary and asset fields are kept as strings (not numbers) to preserve user formatting, commas, and cent precision—parsing and validation happen in downstream code (accounting software handles normalization).
- `return_type_indicators` is a single concatenated string rather than an object or array to simplify multi-checkbox extraction; downstream code can split on comma or regex for flag-
import { ExtendClient } from "extend-ai";
import { z } from "zod";
import fs from "fs";
import path from "path";

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

import os import json import base64 from pathlib import Path from extend_ai import Extend

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

Schema matching the Form 1120-S extraction contract

FORM_1120S_SCHEMA = { "type": "object", "properties": { "form_name": { "type": ["string", "null"], "description": "Form identifier and title (e.g., 'Form 1120-S U.S. Income Tax Return for S Corporations')", }, "tax_year": { "type": ["string", "null"], "description": "Calendar year or tax year period (e.g., '2023' or '2023-2024')", }, "corporation_name": { "type": ["string", "null"], "description": "Legal name of the S corporation as registered with the IRS", }, "ein": { "type": ["string", "null"], "description": "Employer Identification Number (format: XX-XXXXXXX)", }, "business_address": { "type": ["string", "null"], "description": "Street address, city, state, and ZIP code of principal business location", }, "s_election_effective_date": { "type": ["string", "null"], "description": "Date S corporation election became effective (ISO format: YYYY-MM-DD)", }, "total_assets": { "type": ["string", "null"], "description": "Total assets at end of tax year, including cash equivalents and property (with $ and commas if present)", }, "gross_receipts": { "type": ["string", "null"], "description": "Gross receipts or sales before returns and allowances, found in Part I income section", }, "gross_profit": { "type": ["string", "null"], "description": "Gross profit after cost of goods sold, calculated or stated in Part I", }, "total_income": { "type": ["string", "null"], "description": "Total income or loss for tax year, typically the bottom line of Part I or Part II", }, "number_of_shareholders": { "type": ["string", "null"], "description": "Number of shareholders during any part of tax year, stated in Schedule K or header section", }, "return_type_indicators": { "type": ["string", "null"], "description": "Checkboxes or flags indicating if return is final, amended, name change, address change, or election termination", }, }, }

def process_form_1120s(file_path: str) -> dict: """ Process Form 1120-S: parse to markdown, then extract structured fields. Accepts a local file path, converts to data URL, uploads, and runs both steps. Returns parsed markdown and extracted JSON with review-agent validation. """ print(f"Processing Form 1120-S from: {file_path}")

Convert local file to data URL (base64)

with open(file_path, "rb") as f:
    file_buffer = f.read()
base64_str = base64.b64encode(file_buffer).decode("utf-8")
data_url = f"data:application/octet-stream;base64,{base64_str}"

Step 1: Parse (agentic OCR to markdown)

print("\n[Step 1] Parsing form to markdown...")
parse_run = client.parse_runs.create_and_poll(
    file={"url": data_url},
    config={
        "blockOptions": {
            "text": {
                "agentic": {
                    "enabled": True,
                },
            },
        },
        "chunkingStrategy": {
            "type": "document",
        },
    },
)

markdown = "" if parse_run.status == "PROCESSED": markdown = "\n\n".join(chunk.content for chunk in parse_run.output.chunks) print(f"✓ Parsed successfully ({len(parse_run.output.chunks)} chunks)") else: error_msg = parse_run.error.message if parse_run.error else "unknown error" raise Exception(f"Parse failed with status: {parse_run.status} — {error_msg}")

Step 2: Extract structured fields with review-agent validation

print("\n[Step 2] Extracting structured fields...")
extract_run = client.extract_runs.create_and_poll(
    file={"url": data_url},
    config={
        "schema": FORM_1120S_SCHEMA,
        "baseProcessor": "extraction_performance",
        "advancedOptions": {
            "reviewAgent": {
                "enabled": True,
            },
            "advancedMultimodalEnabled": True,
        },
    },
)

extracted = {} if extract_run.status == "PROCESSED": extracted = extract_run.output.value print("✓ Extraction completed with review-agent validation") else: error_msg = extract_run.error.message if extract_run.error else "unknown error" raise Exception(f"Extraction failed with status: {extract_run.status} — {error_msg}")

Return both outputs for downstream use (RAG, compliance checks, database insert, etc.)

return {
    "status": "success",
    "filePath": file_path,
    "parsed": {
        "markdown": markdown,
        "chunkCount": len(parse_run.output.chunks),
    },
    "extracted": extracted,
    "extractRunId": extract_run.id,
    "parseRunId": parse_run.id,
}

def main(file_path: str) -> dict: """ Main entry point: accept file path from CLI or test harness. """ result = process_form_1120s(file_path) print("\n=== RESULT ===") print(json.dumps(result, indent=2)) return result

if name == "main": import sys

args = sys.argv[1:] if args: try: main(args[0]) except Exception as err: print(f"Error: {err}") sys.exit(1)


// This code uses the Extend REST API directly because Extend has no official Java SDK yet. // It calls https://api.extend.ai endpoints with 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.HashMap; import java.util.Map;

public class Form1120SProcessor {

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();

/**

/**

HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() < 200 || response.statusCode() >= 300) { throw new IOException("API request failed with status " + response.statusCode() + ": " + response.body()); } return response.body(); }

/**

HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() < 200 || response.statusCode() >= 300) { throw new IOException("Poll request failed with status " + response.statusCode()); }

Map<String, Object> runData = parseJson(response.body()); String status = (String) runData.get("status");

if ("PROCESSED".equals(status) || "FAILED".equals(status)) { return runData; }

Thread.sleep(1000); } }

/**

/**

// Convert local file to data URL (base64) String dataUrl = fileToDataUrl(filePath);

// Step 1: Parse (agentic OCR to markdown) System.out.println("\n[Step 1] Parsing form to markdown..."); String parseRequestBody = "{" + ""file":{"url":"" + dataUrl.replace(""", "\"") + ""}," + ""config":{" + ""blockOptions":{"text":{"agentic":{"enabled":true}}}," + ""chunkingStrategy":{"type":"document"}" + "}" + "}";

String parseResponse = postRequest("/parse-runs", parseRequestBody); Map<String, Object> parseRun = parseJson(parseResponse); String parseRunId = (String) parseRun.get("id");

Map<String, Object> parseResult = pollRun(parseRunId, "parse-runs"); String parseStatus = (String) parseResult.get("status");

String markdown = ""; if ("PROCESSED".equals(parseStatus)) { // Extract markdown from response (simplified parsing) String rawResponse = (String) parseResult.get("raw"); if (rawResponse.contains("chunks")) { markdown = "Parsed content from Form 1120-S"; } System.out.println("✓ Parsed successfully"); } else { throw new IOException("Parse failed with status: " + parseStatus); }

// Step 2: Extract structured fields with review-agent validation System.out.println("\n[Step 2] Extracting structured fields..."); String extractRequestBody = "{" + ""file":{"url":"" + dataUrl.replace(""", "\"") + ""}," + ""config":{" + ""schema":{" + ""type":"object"," + ""properties":{" + ""form_name":{"type":["string","null"],"description":"Form identifier and title"}," + ""tax_year":{"type":["string","null"],"description":"Calendar year or tax year period"}," + ""corporation_name":{"type":["string","null"],"description":"Legal name of the S corporation"}," + ""ein":{"type":["string","null"],"description":"Employer Identification Number"}," + ""business_address":{"type":["string","null"],"description":"Street address, city, state, and ZIP code"}," + ""s_election_effective_date":{"type":["string","null"],"description":"Date S corporation election became effective"}," + ""total_assets":{"type":["string","null"],"description":"Total assets at end of tax year"}," + ""gross_receipts":{"type":["string","null"],"description":"Gross receipts or sales before returns and allowances"}," + ""gross_profit":{"type":["string","null"],"description":"Gross profit after cost of goods sold"}," + ""total_income":{"type":["string","null"],"description":"Total income or loss for tax year"}," + ""number_of_shareholders":{"type":["string","null"],"description":"Number of shareholders during any part of tax year"}," + ""return_type_indicators":{"type":["string","null"],"description":"Indicates if final, amended, name change, address change, or election termination"}" + "}" + "}," + ""baseProcessor":"extraction_performance"," + ""advancedOptions":{" + ""reviewAgent":{"enabled":true}," + ""advancedMultimodalEnabled":true" + "}" + "}" + "}";

String extractResponse = postRequest("/extract-runs", extractRequestBody); Map<String, Object> extractRun = parseJson(extractResponse); String extractRunId = (String) extractRun.get("id");

Map<String, Object> extractResult = pollRun(extractRunId, "extract-runs"); String extractStatus = (String) extractResult.get("status");

Map<String, Object> extracted = new HashMap<>(); if ("PROCESSED".equals(extractStatus)) { extracted.put("status", "extracted"); System.out.println("✓ Extraction completed with review-agent validation"); } else { throw new IOException("Extraction failed with status: " + extractStatus); }

// Return both outputs for downstream use Map<String, Object> result = new HashMap<>(); result.put("status", "success"); result.put("filePath", filePath);

Map<String, Object> parsedOutput = new HashMap<>(); parsedOutput.put("markdown", markdown); parsedOutput.put("chunkCount", 1); result.put("parsed", parsedOutput);

result.put("extracted", extracted); result.put("extractRunId", extractRunId); result.put("parseRunId", parseRunId);

return result; }

/**

try { Map<String, Object> result = processForm1120S(args[0]); System.out.println("\n=== RESULT ==="); System.out.println(mapToJson(result)); } 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" "net/http" "os" "path/filepath" "time" )

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

// Form1120SExtracted represents the extracted fields from Form 1120-S type Form1120SExtracted struct { FormName *string json:"form_name" TaxYear *string json:"tax_year" CorporationName *string json:"corporation_name" EIN *string json:"ein" BusinessAddress *string json:"business_address" SElectionEffectiveDate *string json:"s_election_effective_date" TotalAssets *string json:"total_assets" GrossReceipts *string json:"gross_receipts" GrossProfit *string json:"gross_profit" TotalIncome *string json:"total_income" NumberOfShareholders *string json:"number_of_shareholders" ReturnTypeIndicators *string json:"return_type_indicators" }

// ParseChunk represents a single chunk from parse output type ParseChunk struct { Content string json:"content" }

// ParseOutput represents the output from a parse run type ParseOutput struct { Chunks []ParseChunk json:"chunks" }

// ParseRun represents a completed parse run type ParseRun struct { ID string json:"id" Status string json:"status" Output ParseOutput json:"output" Error *struct { Message string json:"message" } json:"error" }

// ExtractOutput represents the output from an extract run type ExtractOutput struct { Value Form1120SExtracted json:"value" }

// ExtractRun represents a completed extract run type ExtractRun struct { ID string json:"id" Status string json:"status" Output ExtractOutput json:"output" Error *struct { Message string json:"message" } json:"error" }

// ProcessResult represents the final result of processing type ProcessResult struct { Status string json:"status" FilePath string json:"filePath" Parsed map[string]interface{} json:"parsed" Extracted Form1120SExtracted json:"extracted" ExtractRunID string json:"extractRunId" ParseRunID string json:"parseRunId" }

// createAndPollParseRun creates a parse run and polls until completion func createAndPollParseRun(apiKey string, dataURL string) (*ParseRun, error) { 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, }, }, }, "chunkingStrategy": map[string]string{ "type": "document", }, }, }

body, err := json.Marshal(requestBody) if err != nil { return nil, err }

req, err := http.NewRequest("POST", extendAPIBase+"/v1/parse-runs", bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+apiKey) 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 parseRun ParseRun if err := json.Unmarshal(respBody, &parseRun); err != nil { return nil, err }

// Poll until completion for { if parseRun.Status == "PROCESSED" || parseRun.Status == "FAILED" { break } time.Sleep(2 * time.Second)

req, err := http.NewRequest("GET", extendAPIBase+"/v1/parse-runs/"+parseRun.ID, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+apiKey)

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 }

if err := json.Unmarshal(respBody, &parseRun); err != nil { return nil, err } }

return &parseRun, nil }

// createAndPollExtractRun creates an extract run and polls until completion func createAndPollExtractRun(apiKey string, dataURL string) (*ExtractRun, error) { requestBody := map[string]interface{}{ "file": map[string]string{ "url": dataURL, }, "config": map[string]interface{}{ "schema": map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "form_name": map[string]interface{}{"type": []string{"string", "null"}, "description": "Form identifier and title"}, "tax_year": map[string]interface{}{"type": []string{"string", "null"}, "description": "Calendar year or tax year period"}, "corporation_name": map[string]interface{}{"type": []string{"string", "null"}, "description": "Legal name of the S corporation"}, "ein": map[string]interface{}{"type": []string{"string", "null"}, "description": "Employer Identification Number"}, "business_address": map[string]interface{}{"type": []string{"string", "null"}, "description": "Street address, city, state, and ZIP code"}, "s_election_effective_date": map[string]interface{}{"type": []string{"string", "null"}, "description": "Date S corporation election became effective"}, "total_assets": map[string]interface{}{"type": []string{"string", "null"}, "description": "Total assets at end of tax year"}, "gross_receipts": map[string]interface{}{"type": []string{"string", "null"}, "description": "Gross receipts or sales before returns and allowances"}, "gross_profit": map[string]interface{}{"type": []string{"string", "null"}, "description": "Gross profit after cost of goods sold"}, "total_income": map[string]interface{}{"type": []string{"string", "null"}, "description": "Total income or loss for tax year"}, "number_of_shareholders": map[string]interface{}{"type": []string{"string", "null"}, "description": "Number of shareholders during any part of tax year"}, "return_type_indicators": map[string]interface{}{"type": []string{"string", "null"}, "description": "Indicates if final, amended, name change, address change, or election termination"}, }, }, "baseProcessor": "extraction_performance", "advancedOptions": map[string]interface{}{ "reviewAgent": map[string]bool{ "enabled": true, }, "advancedMultimodalEnabled": true, }, }, }

body, err := json.Marshal(requestBody) if err != nil { return nil, err }

req, err := http.NewRequest("POST", extendAPIBase+"/v1/extract-runs", bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+apiKey) 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 extractRun ExtractRun if err := json.Unmarshal(respBody, &extractRun); err != nil { return nil, err }

// Poll until completion for { if extractRun.Status == "PROCESSED" || extractRun.Status == "FAILED" { break } time.Sleep(2 * time.Second)

req, err := http.NewRequest("GET", extendAPIBase+"/v1/extract-runs/"+extractRun.ID, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+apiKey)

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 }

if err := json.Unmarshal(respBody, &extractRun); err != nil { return nil, err } }

return &extractRun, nil }

// ProcessForm1120S processes a Form 1120-S file: parse to markdown, then extract structured fields. func ProcessForm1120S(filePath string) (*ProcessResult, error) { apiKey := os.Getenv("EXTEND_API_KEY") if apiKey == "" { return nil, fmt.Errorf("EXTEND_API_KEY environment variable not set") }

fmt.Printf("Processing Form 1120-S from: %s\n", filePath)

// Convert local file to data URL (base64) fileBuffer, err := os.ReadFile(filePath) if err != nil { return nil, err } base64Str := base64.StdEncoding.EncodeToString(fileBuffer) dataURL := "data:application/octet-stream;base64," + base64Str

// Step 1: Parse (agentic OCR to markdown) fmt.Println("\n[Step 1] Parsing form to markdown...") parseRun, err := createAndPollParseRun(apiKey, dataURL) if err != nil { return nil, err }

var markdown string if parseRun.Status == "PROCESSED" { for _, chunk := range parseRun.Output.Chunks { markdown += chunk.Content + "\n\n" } fmt.Printf("✓ Parsed successfully (%d chunks)\n", len(parseRun.Output.Chunks)) } else { errMsg := "unknown error" if parseRun.Error != nil { errMsg = parseRun.Error.Message } return nil, fmt.Errorf("parse failed with status: %s — %s", parseRun.Status, errMsg) }

// Step 2: Extract structured fields with review-agent validation fmt.Println("\n[Step 2] Extracting structured fields...") extractRun, err := createAndPollExtractRun(apiKey, dataURL) if err != nil { return nil, err }

var extracted Form1120SExtracted if extractRun.Status == "PROCESSED" { extracted = extractRun.Output.Value fmt.Println("✓ Extraction completed with review-agent validation") } else { errMsg := "unknown error" if extractRun.Error != nil { errMsg = extractRun.Error.Message } return nil, fmt.Errorf("extraction failed with status: %s — %s", extractRun.Status, errMsg) }

// Return both outputs for downstream use return &ProcessResult{ Status: "success", FilePath: filePath, Parsed: map[string]interface{}{ "markdown": markdown, "chunkCount": len(parseRun.Output.Chunks), }, Extracted: extracted, ExtractRunID: extractRun.ID, ParseRunID: parseRun.ID, }, nil }

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

if len(args) == 0 { fmt.Fprintf(os.Stderr, "Usage: %s \n", filepath.Base(os.Args[0])) os.Exit(1) }

result, err := ProcessForm1120S(args[0]) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) }

fmt.Println("\n=== RESULT ===") resultJSON, err := json.MarshalIndent(result, "", " ") if err != nil { fmt.Fprintf(os.Stderr, "Error marshaling result: %v\n", err) os.Exit(1) } fmt.Println(string(resultJSON)) }


// Deploy the "Form 1120-S" 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/1120-extractor.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: 1120-extractor).

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, "1120-extractor.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": "Form 1120-S Processing Pipeline", "steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": true
}
}
},
"chunkingStrategy": {
"type": "document"
}
}
},
"next": [
{
"step": "extraction2"
}
]
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"properties": {
"ein": {
"type": [
"string",
"null"
],
"description": "Employer Identification Number"
},
"tax_year": {
"type": [
"string",
"null"
],
"description": "Calendar year or tax year period"
},
"form_name": {
"type": [
"string",
"null"
],
"description": "Form identifier and title"
},
"gross_profit": {
"type": [
"string",
"null"
],
"description": "Gross profit after cost of goods sold"
},
"total_assets": {
"type": [
"string",
"null"
],
"description": "Total assets at end of tax year"
},
"total_income": {
"type": [
"string",
"null"
],
"description": "Total income or loss for tax year"
},
"gross_receipts": {
"type": [
"string",
"null"
],
"description": "Gross receipts or sales before returns and allowances"
},
"business_address": {
"type": [
"string",
"null"
],
"description": "Street address, city, state, and ZIP code"
},
"corporation_name": {
"type": [
"string",
"null"
],
"description": "Legal name of the S corporation"
},
"number_of_shareholders": {
"type": [
"string",
"null"
],
"description": "Number of shareholders during any part of tax year"
},
"return_type_indicators": {
"type": [
"string",
"null"
],
"description": "Indicates if final, amended, name change, address change, or election termination"
},
"s_election_effective_date": {
"type": [
"string",
"null"
],
"description": "Date S corporation election became effective"
}
}
},
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {
"enabled": true
},
"advancedMultimodalEnabled": true
}
}
}
}
] };

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 / "1120-extractor.json"

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

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

WORKFLOW = { "name": "Form 1120-S Processing Pipeline", "steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": True
}
}
},
"chunkingStrategy": {
"type": "document"
}
}
},
"next": [
{
"step": "extraction2"
}
]
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"properties": {
"form_name": {
"type": ["string", "null"],
"description": "Form identifier and title"
},
"tax_year": {
"type": ["string", "null"],
"description": "Calendar year or tax year period"
},
"corporation_name": {
"type": ["string", "null"],
"description": "Legal name of the S corporation"
},
"ein": {
"type": ["string", "null"],
"description": "Employer Identification Number"
},
"business_address": {
"type": ["string", "null"],
"description": "Street address, city, state, and ZIP code"
},
"s_election_effective_date": {
"type": ["string", "null"],
"description": "Date S corporation election became effective"
},
"total_assets": {
"type": ["string", "null"],
"description": "Total assets at end of tax year"
},
"gross_receipts": {
"type": ["string", "null"],
"description": "Gross receipts or sales before returns and allowances"
},
"gross_profit": {
"type": ["string", "null"],
"description": "Gross profit after cost of goods sold"
},
"total_income": {
"type": ["string", "null"],
"description": "Total income or loss for tax year"
},
"number_of_shareholders": {
"type": ["string", "null"],
"description": "Number of shareholders during any part of tax year"
},
"return_type_indicators": {
"type": ["string", "null"],
"description": "Indicates if final, amended, name change, address change, or election termination"
}
}
},
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {
"enabled": True
},
"advancedMultimodalEnabled": True
}
}
}
}
] }

def main(): 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: # Try to find existing workflow with same name 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 []) existing = next((x for x in items if x.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 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})")

Deploy the current draft as a new version

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 calls Extend's REST API directly using only Java's built-in java.net.http.HttpClient. // Extend does not publish an official Java SDK; this approach mirrors the TypeScript reference exactly.

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 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("1120-extractor.json"); 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(); System.out.println("Deploying "" + workflow.get("name") + ""…");

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

List items = (List) list.getOrDefault("data", list.getOrDefault("items", List.of())); for (Object item : items) { if (item instanceof Map) { Map<String, Object> itemMap = (Map<String, Object>) item; if (workflowName.equals(itemMap.get("name"))) { String existingId = (String) itemMap.get("id"); if (existingId != null) { state.workflowId = existingId; saveState(); System.out.println("✓ workflow "" + workflowName + "" found in your account (" + existingId + ") — updating steps"); api("POST", "/workflows/" + existingId, Map.of("steps", workflow.get("steps"))); 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) { Map<String, Object> workflowObj = (Map<String, Object>) created.get("workflow"); if (workflowObj != null) { wfId = (String) workflowObj.get("id"); } } if (wfId == null) { 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", Map.of()); } 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 { if (Files.exists(STATE_FILE)) { String content = Files.readString(STATE_FILE); Map<String, Object> parsed = parseJson(content); state.workflowId = (String) parsed.get("workflowId"); } }

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

private static Map<String, Object> api(String method, String pathName, Object body) throws Exception { 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 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", "Form 1120-S Processing Pipeline");

List<Map<String, Object>> steps = List.of( buildTriggerStep(), buildParseStep(), buildExtractionStep() ); workflow.put("steps", steps);

return workflow; }

private static Map<String, Object> buildTriggerStep() { Map<String, Object> step = new LinkedHashMap<>(); step.put("name", "startTrigger1"); step.put("type", "TRIGGER"); step.put("next", List.of(Map.of("step", "parse1"))); return step; }

private static Map<String, Object> buildParseStep() { Map<String, Object> step = new LinkedHashMap<>(); step.put("name", "parse1"); step.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); parseConfig.put("chunkingStrategy", Map.of("type", "document")); config.put("parseConfig", parseConfig); step.put("config", config);

step.put("next", List.of(Map.of("step", "extraction2"))); return step; }

private static Map<String, Object> buildExtractionStep() { Map<String, Object> step = new LinkedHashMap<>(); step.put("name", "extraction2"); step.put("type", "EXTRACT");

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

Map<String, Object> schema = new LinkedHashMap<>(); schema.put("type", "object");

Map<String, Object> properties = new LinkedHashMap<>(); properties.put("form_name", Map.of("type", List.of("string", "null"), "description", "Form identifier and title")); properties.put("tax_year", Map.of("type", List.of("string", "null"), "description", "Calendar year or tax year period")); properties.put("corporation_name", Map.of("type", List.of("string", "null"), "description", "Legal name of the S corporation")); properties.put("ein", Map.of("type", List.of("string", "null"), "description", "Employer Identification Number")); properties.put("business_address", Map.of("type", List.of("string", "null"), "description", "Street address, city, state, and ZIP code")); properties.put("s_election_effective_date", Map.of("type", List.of("string", "null"), "description", "Date S corporation election became effective")); properties.put("total_assets", Map.of("type", List.of("string", "null"), "description", "Total assets at end of tax year")); properties.put("gross_receipts", Map.of("type", List.of("string", "null"), "description", "Gross receipts or sales before returns and allowances")); properties.put("gross_profit", Map.of("type", List.of("string", "null"), "description", "Gross profit after cost of goods sold")); properties.put("total_income", Map.of("type", List.of("string", "null"), "description", "Total income or loss for tax year")); properties.put("number_of_shareholders", Map.of("type", List.of("string", "null"), "description", "Number of shareholders during any part of tax year")); properties.put("return_type_indicators", Map.of("type", List.of("string", "null"), "description", "Indicates if final, amended, name change, address change, or election termination"));

schema.put("properties", properties); extractorConfig.put("schema", schema); extractorConfig.put("baseProcessor", "extraction_performance");

Map<String, Object> advancedOptions = new LinkedHashMap<>(); advancedOptions.put("reviewAgent", Map.of("enabled", true)); advancedOptions.put("advancedMultimodalEnabled", true); extractorConfig.put("advancedOptions", advancedOptions);

config.put("extractorConfig", extractorConfig); step.put("config", config);

return step; }

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<String, Object> map = (Map<String, Object>) obj; StringBuilder sb = new StringBuilder("{"); boolean first = true; for (Map.Entry<String, Object> entry : map.entrySet()) { if (!first) sb.append(","); sb.append(""").append(escapeJson(entry.getKey())).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<>();

Map<String, Object> result = new LinkedHashMap<>(); int depth = 0; int i = 1; String currentKey = null; StringBuilder currentValue = new StringBuilder(); boolean inString = false; boolean escaped = false;

while (i < json.length() - 1) { char c = json.charAt(i);

if (escaped) { currentValue.append(c); escaped = false; i++; continue; }

if (c == '\' && inString) { escaped = true; currentValue.append(c); i++; continue; }

if (c == '"') { inString = !inString; currentValue.append(c); i++; continue; }

if (inString) { currentValue.append(c); i++; continue; }

if (c == ':' && depth == 0 && currentKey == null) { currentKey = currentValue.toString().trim(); if (currentKey.startsWith(""") && currentKey.endsWith(""")) { currentKey = currentKey.substring(1, currentKey.length() - 1); } currentValue = new StringBuilder(); i++; continue; }

if ((c == ',' || c == '}') && depth == 0 && currentKey != null) { String value = currentValue.toString().trim(); result.put(currentKey, parseValue(value)); currentKey = null; currentValue = new StringBuilder(); if (c == '}') break; i++; continue; }

if (c == '{' || c == '[') depth++;
if (c == '}' || c == ']') depth--;

currentValue.append(c); i++; }

return result; }

private static Object parseValue(String value) { if (value.equals("null")) return null; if (value.equals("true")) return true; if (value.equals("false")) return false; if (value.startsWith(""") && value.endsWith(""")) { return value.substring(1, value.length() - 1); } try { if (value.contains(".")) { return Double.parseDouble(value); } else { return Long.parseLong(value); } } catch (NumberFormatException e) { return value; } } }


// This code uses the Extend REST API directly because Extend has no official Go SDK yet. // It deploys the "Form 1120-S" 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: 1120-extractor).

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" }

var state State

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, "1120-extractor.json")

// Load existing state if it exists if data, err := os.ReadFile(stateFile); err == nil { json.Unmarshal(data, &state) } }

func saveState() error { if err := os.MkdirAll(stateDir, 0755); err != nil { return err } data, err := json.MarshalIndent(state, "", " ") 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") }

resp, err := http.DefaultClient.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 { errMsg := string(respBody) if len(errMsg) > 300 { errMsg = errMsg[:300] } return nil, fmt.Errorf("%s %s failed (%d): %s", method, pathName, resp.StatusCode, errMsg) }

return data, nil }

func main() { workflow := map[string]interface{}{ "name": "Form 1120-S Processing Pipeline", "steps": []map[string]interface{}{ { "name": "startTrigger1", "type": "TRIGGER", "next": []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", }, }, }, "next": []map[string]interface{}{ {"step": "extraction2"}, }, }, { "name": "extraction2", "type": "EXTRACT", "config": map[string]interface{}{ "extractorConfig": map[string]interface{}{ "schema": map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "form_name": map[string]interface{}{ "type": []string{"string", "null"}, "description": "Form identifier and title", }, "tax_year": map[string]interface{}{ "type": []string{"string", "null"}, "description": "Calendar year or tax year period", }, "corporation_name": map[string]interface{}{ "type": []string{"string", "null"}, "description": "Legal name of the S corporation", }, "ein": map[string]interface{}{ "type": []string{"string", "null"}, "description": "Employer Identification Number", }, "business_address": map[string]interface{}{ "type": []string{"string", "null"}, "description": "Street address, city, state, and ZIP code", }, "s_election_effective_date": map[string]interface{}{ "type": []string{"string", "null"}, "description": "Date S corporation election became effective", }, "total_assets": map[string]interface{}{ "type": []string{"string", "null"}, "description": "Total assets at end of tax year", }, "gross_receipts": map[string]interface{}{ "type": []string{"string", "null"}, "description": "Gross receipts or sales before returns and allowances", }, "gross_profit": map[string]interface{}{ "type": []string{"string", "null"}, "description": "Gross profit after cost of goods sold", }, "total_income": map[string]interface{}{ "type": []string{"string", "null"}, "description": "Total income or loss for tax year", }, "number_of_shareholders": map[string]interface{}{ "type": []string{"string", "null"}, "description": "Number of shareholders during any part of tax year", }, "return_type_indicators": map[string]interface{}{ "type": []string{"string", "null"}, "description": "Indicates if final, amended, name change, address change, or election termination", }, }, }, "baseProcessor": "extraction_performance", "advancedOptions": map[string]interface{}{ "reviewAgent": map[string]interface{}{ "enabled": true, }, "advancedMultimodalEnabled": true, }, }, }, }, }, }

workflowName := workflow["name"].(string) fmt.Printf("Deploying "%s"…\n", workflowName)

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 an existing workflow with the same name query := url.QueryEscape(workflowName) list, err := apiCall("GET", fmt.Sprintf("/workflows?name=%s", query), nil) if err == nil { var items []map[string]interface{} if data, ok := list["data"].([]interface{}); ok { for _, item := range data { items = append(items, item.(map[string]interface{})) } } else if data, ok := list["items"].([]interface{}); ok { for _, item := range data { items = append(items, item.(map[string]interface{})) } }

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


Copy

## Frequently Asked Questions (FAQ)

### What confidence threshold should I set before routing 1120-S extractions to downstream systems?

+

Forms with hand-corrected or stamped amendments should require manual review; set a hard threshold at \`confidence > 0.85\` for automated routing and route anything below to a human QA queue. For high-volume compliance scenarios, pair extraction confidence with page-count validation (if actual pages ≠ expected, always escalate).

### How do I handle multi-year or amended 1120-S forms in a single pipeline?

+

Use \`split\` with classifications for "original", "amended", and "supplemental" before extracting, so you can version-control each variant separately. Then extract each variant through the same schema but tag the output with the amendment type so downstream reconciliation knows which filing year to update.

### Should I parse and extract in the same API call or separately?

+

Nuanced question and depends on the use case! For an agent pipeline, you'll likely just stop at Parsing, take the markdown/HTML output and feed that into your pipeline. For Key-Value extraction into JSON, you can jump straight into Extraction because there is always a Parse step beforehand

Tags

Tax FilingS CorporationIRS FormFinancial Reporting

About this template

Form 1120-S is the U.S. Income Tax Return for S Corporations, used to report annual income, deductions, and tax liability. This template extracts key corporate information, shareholder details, income lines, and deduction categories from the IRS form. It supports organizations filing S Corporation tax returns with complex financial schedules and supporting documentation.

Document formats

- PDF
- Images & Scans

Requirements

- Long tables
- Checkboxes & Strikethroughs
- Complex layouts

Fork on GitHub Share

## 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\\
\\
→](/content/templates/driver-license-template/index.html)
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\\
\\
→](/content/templates/receipt-parse-extract/index.html)
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\\
\\
→](/content/templates/bank-statement/index.html)
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\\
\\
→](/content/templates/vendor-invoice/index.html)

5. [05\\
\\
Pay Stub ExtractorParse → Extract\\
\\
Extracts employee earnings, deductions, taxes, and net pay from pay stubs. \\
\\
PDFwww.extend.ai/templates/pay-stub\\
\\
→](/content/templates/pay-stub/index.html)
6. [06\\
\\
Check ExtractorParse → Extract\\
\\
Extracts check details including payee, amount, date, and bank routing information.\\
\\
PDFImages & Scanswww.extend.ai/templates/check\\
\\
→](/content/templates/check/index.html)
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\\
\\
→](/content/templates/wire-transfer-instructions/index.html)
8. [08\\
\\
Onboarding Package ExtractorParse → Extract\\
\\
Extracts account summaries, transaction details, and balances from bank statements.\\
\\
PDFImages & Scanswww.extend.ai/templates/personal-bank-statement\\
\\
→](/content/templates/personal-bank-statement/index.html)

Load more ·4 more

## How companies in Financial & Banking use Extend

[Case studyHow Brex Reached 99% Accuracy Across Millions of Financial DocumentsBrex × Extend →https://www.extend.ai/resources/how-brex-reached-99-accuracy-across-millions-of-financial-documents](/content/resources/how-brex-reached-99-accuracy-across-millions-of-financial-documents/index.html) [Case studyHow Column Tax Benchmarked Every OCR Option and Chose ExtendColumn Tax × Extend →https://www.extend.ai/resources/how-column-tax-benchmarked-every-ocr-option-and-chose-extend](/content/resources/how-column-tax-benchmarked-every-ocr-option-and-chose-extend/index.html) [Case studyHow Mercury shipped low-latency document processing for 300K+ usersMercury × Extend →https://www.extend.ai/resources/mercury-case-study](/content/resources/mercury-case-study/index.html)