Extracts personal and dependent information from U.S. individual income tax returns.
Form 1040 is the primary annual tax return filed by U.S. individuals with the Internal Revenue Service, containing taxpayer and spouse identification, filing status, dependent information, residency status, digital asset disclosures, and tax election preferences required for federal income tax compliance. This template takes in Form 1040 U.S. Individual Income Tax Return and outputs markdown (.md) capturing the parsed form text and structure, and JSON (.json) with extracted taxpayer identification, filing status, dependent count, address, residency status, digital asset disclosure, and tax election fields per the extraction schema by using Extend's Parse, Extract primitives.
Converts the document into clean, layout-aware markdown plus structured blocks with spatial metadata.
blockOptions.text.agentic.enabledtruechangedchunkingStrategy.type"document"engine"parse_performance"You can learn more about Parse configuration in Extend's Parse documentation.
Pulls a defined set of fields from the document and returns them as structured JSON matching a schema.
schemacustom schema — 12 fieldschangedadvancedOptions.advancedMultimodalEnabledtruechangedadvancedOptions.reviewAgent.enabledtruechangedbaseProcessor"extraction_performance"You can learn more about Extract configuration in Extend's Extract documentation.
{
"name": "Form 1040 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": {
"address": {
"type": [
"string",
"null"
],
"description": "Home address including street, city, state, and ZIP code"
},
"tax_year": {
"type": [
"string",
"null"
],
"description": "Tax year for the return (e.g., 2025)"
},
"spouse_ssn": {
"type": [
"string",
"null"
],
"description": "Spouse's social security number"
},
"spouse_name": {
"type": [
"string",
"null"
],
"description": "Spouse's first name, middle initial, and last name"
},
"taxpayer_ssn": {
"type": [
"string",
"null"
],
"description": "Taxpayer's social security number"
},
"filing_status": {
"type": [
"string",
"null"
],
"description": "Filing status (Single, Married filing jointly, Married filing separately, Head of household, or Qualifying surviving spouse)"
},
"digital_assets": {
"type": [
"string",
"null"
],
"description": "Whether taxpayer received, sold, or disposed of digital assets during tax year (Yes/No)"
},
"dependents_count": {
"type": [
"string",
"null"
],
"description": "Number of dependents claimed on return"
},
"taxpayer_last_name": {
"type": [
"string",
"null"
],
"description": "Taxpayer's last name"
},
"us_resident_status": {
"type": [
"string",
"null"
],
"description": "Whether main home was in U.S. for more than half of tax year"
},
"taxpayer_first_name": {
"type": [
"string",
"null"
],
"description": "Taxpayer's first name and middle initial"
},
"presidential_election_campaign": {
"type": [
"string",
"null"
],
"description": "Whether taxpayer wants $3 to go to presidential election campaign fund"
}
}
},
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {
"enabled": true
},
"advancedMultimodalEnabled": true
}
}
}
}
]
}# Form 1040 Processing — Extend AI Skill
## What this pipeline does
This pipeline converts a scanned or digital Form 1040 (U.S. individual income tax return) into structured, compliance-ready data. It parses the entire form to markdown using agentic OCR (to handle handwriting and complex layouts), then extracts 11 key tax fields—taxpayer identity, filing status, dependent count, digital asset disclosures, and IRS election preferences—into a JSON object suitable for downstream tax processing systems, e-filing platforms, or CRM ingestion.
## When to use this
- **Tax prep software intake**: Automatically populate client intake forms from scanned 1040s submitted by email or portal upload.
- **IRS compliance audit trails**: Extract taxpayer identity and filing metadata for record-keeping and audit trail generation.
- **Form assembly & e-filing**: Pull structured data to prefill subsequent tax forms (Schedules A–D, Form 8949, etc.) or feed directly into e-filing systems.
- **Tax services workflow automation**: Reduce manual data entry by 90% when processing batches of client-submitted 1040s during tax season.
- **Digital asset reporting**: Automatically detect whether a taxpayer disclosed cryptocurrency/NFT activity (required for Form 8949 correlation).
## Processor pipeline
### Step 1: Parse (agentic OCR)
- **Processor**: `parse_performance` with agentic text extraction enabled
- **Purpose**: Convert the entire 1040 form—whether handwritten, printed, or hybrid—into machine-readable markdown with accurate field boundaries and spacing preserved.
- **Key config**: `blockOptions.text.agentic.enabled: true` and `chunkingStrategy.type: "document"` ensure the entire form is processed as a cohesive unit, respecting the IRS's multi-section layout (header, income, deductions, tax computation, signature block).
- **Why this config**: Form 1040 often includes handwritten entries, small print, checkbox states, and complex tabular structures. Agentic OCR detects these reliably; document-level chunking prevents splitting mid-field and losing context.
### Step 2: Extract (structured fields with review agent)
- **Processor**: `extraction_performance` with `reviewAgent.enabled: true` and `advancedMultimodalEnabled: true`
- **Purpose**: Pull 11 critical fields—SSN, filing status, dependent count, digital asset disclosure—into typed JSON, with AI review to flag low-confidence extractions.
- **Key config**: `baseProcessor: "extraction_performance"` prioritizes accuracy over speed; `reviewAgent` flags mismatches (e.g., spouse SSN present but filing status is "Single") for human review before downstream filing.
- **Why this config**: SSN and filing status are high-stakes fields; a single error blocks e-filing or creates audit risk. The review agent catches semantic inconsistencies; advanced multimodal processing handles mixed text/checkbox/signature scenarios.
## TypeScript implementation
```typescript
import { ExtendClient } from "extend-ai";
import { z } from "zod";
import fs from "fs";
const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });
/**
* Process a Form 1040 from a local file:
* 1. Parse to markdown using agentic OCR
* 2. Extract structured tax fields
*/
async function processForm1040(filePath: string) {
// Convert local file to base64 data URL for SDK consumption
const fileBuffer = fs.readFileSync(filePath);
const base64 = fileBuffer.toString("base64");
const dataUrl = `data:application/octet-stream;base64,${base64}`;
console.log(`Processing Form 1040 from ${filePath}...`);
// Step 1: Parse the form to markdown with agentic OCR
console.log("Step 1: Parsing form with agentic OCR...");
const parseRun = await client.parseRuns.createAndPoll({
file: { url: dataUrl },
config: {
blockOptions: {
text: {
agentic: {
enabled: true,
},
},
},
chunkingStrategy: {
type: "document",
},
},
});
if (parseRun.status !== "PROCESSED") {
throw new Error(`Parse failed with status: ${parseRun.status}`);
}
// Collect parsed markdown for logging/debugging
const parsedMarkdown = parseRun.output.chunks
.map((chunk) => chunk.content)
.join("\n\n");
console.log(`✓ Parsed to ${parseRun.output.chunks.length} chunks`);
// Step 2: Extract structured fields using Zod schema + review agent
console.log("Step 2: Extracting Form 1040 fields...");
const extractRun = await client.extractRuns.createAndPoll({
file: { url: dataUrl },
config: {
schema: z.object({
tax_year: z
.string()
.nullable()
.describe(
"Tax year for the return (e.g., 2025). Found in header or form identifier."
),
taxpayer_first_name: z
.string()
.nullable()
.describe(
"Taxpayer's first name and middle initial. Located in top-left section, before last name field."
),
taxpayer_last_name: z
.string()
.nullable()
.describe(
"Taxpayer's last name. Located immediately after first name field in header."
),
taxpayer_ssn: z
.string()
.nullable()
.describe(
"Taxpayer's social security number (9 digits, format XXX-XX-XXXX or no dashes). Critical for IRS matching."
),
spouse_name: z
.string()
.nullable()
.describe(
"Spouse's first name, middle initial, and last name. Only present if filing jointly or separately; leave null if filing single/HOH."
),
spouse_ssn: z
.string()
.nullable()
.describe(
"Spouse's social security number (9 digits). Only present if filing status is joint or separately; otherwise null."
),
address: z
.string()
.nullable()
.describe(
"Home address including street, city, state, and ZIP code. Found in 'Your address' section; format as single line or preserve line breaks."
),
filing_status: z
.string()
.nullable()
.describe(
"Filing status: must be one of 'Single', 'Married filing jointly', 'Married filing separately', 'Head of household', or 'Qualifying surviving spouse'. Inferred from checkbox marks or text entry."
),
digital_assets: z
.string()
.nullable()
.describe(
"Whether taxpayer received, sold, or disposed of digital assets (cryptocurrency, NFT, etc.) during tax year. Answer is 'Yes' or 'No'; critical for Schedule D / Form 8949 triggering."
),
presidential_election_campaign: z
.string()
.nullable()
.describe(
"Whether taxpayer wants $3 to go to presidential election campaign fund. Answer is 'Yes' or 'No' based on checkbox or explicit election."
),
dependents_count: z
.string()
.nullable()
.describe(
"Number of dependents claimed on return (count of qualifying children/relatives). Extract as string, e.g., '2', '0', or null if blank."
),
us_resident_status: z
.string()
.nullable()
.describe(
"Whether main home was in U.S. for more than half of tax year. Answer is 'Yes' or 'No'; determines residency test for filing status and dependent qualifications."
),
}),
baseProcessor: "extraction_performance",
advancedOptions: {
reviewAgent: {
enabled: true,
},
advancedMultimodalEnabled: true,
},
},
});
if (extractRun.status !== "PROCESSED") {
throw new Error(`Extraction failed with status: ${extractRun.status}`);
}
const extracted = extractRun.output.value;
console.log("✓ Extraction complete");
// Step 3: Output results
console.log("\n=== Form 1040 Extraction Results ===\n");
console.log(JSON.stringify(extracted, null, 2));
// Return data for programmatic consumption
return {
parsed_markdown: parsedMarkdown,
extracted_fields: extracted,
parse_status: parseRun.status,
extract_status: extractRun.status,
};
}
// Invoke if run directly
const filePath = process.argv[2] || "__FILE_PATH__";
processForm1040(filePath).catch(console.error);
```
## CLI equivalent
```bash
# Set API key
export EXTEND_API_KEY="sk_..."
# Step 1: Parse Form 1040 to markdown
extend parse form_1040.pdf
# Step 2: Extract structured fields using inline Zod schema
# (Save the schema to a file first, or use the SDK directly for full control)
extend extract form_1040.pdf \
--schema form1040_schema.json \
--processor extraction_performance \
--review-agent-enabled true \
--advanced-multimodal-enabled true
```
**Note**: The CLI does not expose `blockOptions.text.agentic` or `reviewAgent` directly in flags; for production with these options, use the TypeScript SDK as shown above.
## Schema
```json
{
"type": "object",
"properties": {
"tax_year": {
"type": ["string", "null"],
"description": "Tax year for the return (e.g., 2025). Found in header or form identifier."
},
"taxpayer_first_name": {
"type": ["string", "null"],
"description": "Taxpayer's first name and middle initial. Located in top-left section, before last name field."
},
"taxpayer_last_name": {
"type": ["string", "null"],
"description": "Taxpayer's last name. Located immediately after first name field in header."
},
"taxpayer_ssn": {
"type": ["string", "null"],
"description": "Taxpayer's social security number (9 digits, format XXX-XX-XXXX or no dashes). Critical for IRS matching."
},
"spouse_name": {
"type": ["string", "null"],
"description": "Spouse's first name, middle initial, and last name. Only present if filing jointly or separately; leave null if filing single/HOH."
},
"spouse_ssn": {
"type": ["string", "null"],
"description": "Spouse's social security number (9 digits). Only present if filing status is joint or separately; otherwise null."
},
"address": {
"type": ["string", "null"],
"description": "Home address including street, city, state, and ZIP code. Found in 'Your address' section; format as single line or preserve line breaks."
},
"filing_status": {
"type": ["string", "null"],
"description": "Filing status: must be one of 'Single', 'Married filing jointly', 'Married filing separately', 'Head of household', or 'Qualifying surviving spouse'. Inferred from checkbox marks or text entry."
},
"digital_assets": {
"type": ["string", "null"],
"description": "Whether taxpayer received, sold, or disposed of digital assets (cryptocurrency, NFT, etc.) during tax year. Answer is 'Yes' or 'No'; critical for Schedule D / Form 8949 triggering."
},
"presidential_election_campaign": {
"type": ["string", "null"],
"description": "Whether taxpayer wants $3 to go to presidential election campaign fund. Answer is 'Yes' or 'No' based on checkbox or explicit election."
},
"dependents_count": {
"type": ["string", "null"],
"description": "Number of dependents claimed on return (count of qualifying children/relatives). Extract as string, e.g., '2', '0', or null if blank."
},
"us_resident_status": {
"type": ["string", "null"],
"description": "Whether main home was in U.S. for more than half of tax year. Answer is 'Yes' or 'No'; determines residency test for filing status and dependent qualifications."
}
}
}
```
### Schema field explanations
- **tax_year**: Enables year-over-year tracking and amendment detection (e.g., amended 2024 vs. original 2024). Null if header illegible.
- **taxpayer_first_name** + **taxpayer_last_name** + **taxpayer_ssn**: The trinity of PII for e-filing; SSN in particular must match IRS records exactly or filing is rejected. Names are parsed from the header; if handwritten, agentic OCR is essential.
- **spouse_name** + **spouse_ssn**: Present only for MFJ/MFS filers. If `filing_status` is "Single" or "Head of household" and spouse fields are non-null, the review agent flags this inconsistency.
- **address**: Used for IRS correspondence and audit trail; compare against prior-year address to flag potential fraud.
- **filing_status**: Determines tax table selection, standard deduction amount, and eligibility for credits. Must match spouse SSN presence (MFJ ↔ two SSNs; Single ↔ one SSN).
- **digital_assets**: Yes/No answer; if "Yes", downstream systems auto-trigger Form 8949 (Sales of Capital Assets) extraction. High-stakes field for crypto traders.
- **presidential_election_campaign**: Low-stakes but high-volume field; checkboxes often unchecked. Null means taxpayer did not make an election (IRS default: no fund allocation).
- **dependents_count**: Must be a non-negative integer. Compare against Schedule 1 dependent list for consistency. If mismatch, flag for human review.
- **us_resident_status**: Determines whether all 12 months must be U.S.-based or pro-rata. Non-residents use Form 1040-NR instead; if status is "No" and form is 1040, flag discrepancy.
## Accuracy tips
1. **Agentic OCR is non-negotiable**: Form 1040 includes checkboxes, small print in margins, and often handwritten SSNs. Disable `agentic` mode only if processing clean digital-only forms in bulk; always enable for mixed mail/portal submissions.
2. **SSN format normalization**: Instruct downstream systems to strip dashes and compare as 9-digit strings. Include a regex validator: `^\d{9}$` or `^\d{3}-\d{2}-\d{4}$`. Flag any non-digit characters as OCR corruption.
3. **Filing status + spouse SSN semantic check**: Build a post-extraction validator:
- If `filing_status ∈ ["Single", "Head of household"]`, then `spouse_name` and `spouse_ssn` must be null.
- If `filing_status ∈ ["Married filing jointly", "Married filing separately"]`, then both must be non-null.
- If mismatch, reject extraction and request human review via the review agent.
4. **Dependent count consistency**: After extraction, call a Schedule 1 extract (if attached) or ask user to confirm dependent list. If counts diverge by >1, flag.
5. **Digital assets disclosure is a compliance gate**: Yes/No answers must be binary and explicit. If ambiguous (e.g., "maybe" or blank), escalate to human for IRS audit risk mitigation.
6. **Address parsing**: Expect mixed formats (single-line, multi-line, with/without ZIP+4). Post-process with USPS address validation API before storing; flag unrecognized addresses.
7. **Handwriting confidence signals**: If agentic OCR confidence for SSN or name is <90%, auto-flag for human verification. The review agent surfaces this; configure your workflow to require sign-off before e-filing.
8. **Tax year inference**: If header is illegible, infer from form version ID (e.g., "2024" form = 2024 tax year). Cross-check against file submission date for reasonableness.
## Trade-offs & alternatives
| Scenario | Choice | Why |import { ExtendClient } from "extend-ai";
import { z } from "zod";
import fs from "fs";
const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });
/**
* Process a Form 1040 from a local file:
* 1. Parse to markdown using agentic OCR
* 2. Extract structured tax fields
*/
async function processForm1040(filePath: string) {
// Convert local file to base64 data URL for SDK consumption
const fileBuffer = fs.readFileSync(filePath);
const base64 = fileBuffer.toString("base64");
const dataUrl = `data:application/octet-stream;base64,${base64}`;
console.log(`Processing Form 1040 from ${filePath}...`);
// Step 1: Parse the form to markdown with agentic OCR
console.log("Step 1: Parsing form with agentic OCR...");
const parseRun = await client.parseRuns.createAndPoll({
file: { url: dataUrl },
config: {
blockOptions: {
text: {
agentic: {
enabled: true,
},
},
},
chunkingStrategy: {
type: "document",
},
},
});
if (parseRun.status !== "PROCESSED") {
throw new Error(`Parse failed with status: ${parseRun.status}`);
}
// Collect parsed markdown for logging/debugging
const parsedMarkdown = parseRun.output.chunks
.map((chunk) => chunk.content)
.join("\n\n");
console.log(`✓ Parsed to ${parseRun.output.chunks.length} chunks`);
// Step 2: Extract structured fields using Zod schema + review agent
console.log("Step 2: Extracting Form 1040 fields...");
const extractRun = await client.extractRuns.createAndPoll({
file: { url: dataUrl },
config: {
schema: z.object({
tax_year: z
.string()
.nullable()
.describe(
"Tax year for the return (e.g., 2025). Found in header or form identifier."
),
taxpayer_first_name: z
.string()
.nullable()
.describe(
"Taxpayer's first name and middle initial. Located in top-left section, before last name field."
),
taxpayer_last_name: z
.string()
.nullable()
.describe(
"Taxpayer's last name. Located immediately after first name field in header."
),
taxpayer_ssn: z
.string()
.nullable()
.describe(
"Taxpayer's social security number (9 digits, format XXX-XX-XXXX or no dashes). Critical for IRS matching."
),
spouse_name: z
.string()
.nullable()
.describe(
"Spouse's first name, middle initial, and last name. Only present if filing jointly or separately; leave null if filing single/HOH."
),
spouse_ssn: z
.string()
.nullable()
.describe(
"Spouse's social security number (9 digits). Only present if filing status is joint or separately; otherwise null."
),
address: z
.string()
.nullable()
.describe(
"Home address including street, city, state, and ZIP code. Found in 'Your address' section; format as single line or preserve line breaks."
),
filing_status: z
.string()
.nullable()
.describe(
"Filing status: must be one of 'Single', 'Married filing jointly', 'Married filing separately', 'Head of household', or 'Qualifying surviving spouse'. Inferred from checkbox marks or text entry."
),
digital_assets: z
.string()
.nullable()
.describe(
"Whether taxpayer received, sold, or disposed of digital assets (cryptocurrency, NFT, etc.) during tax year. Answer is 'Yes' or 'No'; critical for Schedule D / Form 8949 triggering."
),
presidential_election_campaign: z
.string()
.nullable()
.describe(
"Whether taxpayer wants $3 to go to presidential election campaign fund. Answer is 'Yes' or 'No' based on checkbox or explicit election."
),
dependents_count: z
.string()
.nullable()
.describe(
"Number of dependents claimed on return (count of qualifying children/relatives). Extract as string, e.g., '2', '0', or null if blank."
),
us_resident_status: z
.string()
.nullable()
.describe(
"Whether main home was in U.S. for more than half of tax year. Answer is 'Yes' or 'No'; determines residency test for filing status and dependent qualifications."
),
}),
baseProcessor: "extraction_performance",
advancedOptions: {
reviewAgent: {
enabled: true,
},
advancedMultimodalEnabled: true,
},
},
});
if (extractRun.status !== "PROCESSED") {
throw new Error(`Extraction failed with status: ${extractRun.status}`);
}
const extracted = extractRun.output.value;
console.log("✓ Extraction complete");
// Step 3: Output results
console.log("\n=== Form 1040 Extraction Results ===\n");
console.log(JSON.stringify(extracted, null, 2));
// Return data for programmatic consumption
return {
parsed_markdown: parsedMarkdown,
extracted_fields: extracted,
parse_status: parseRun.status,
extract_status: extractRun.status,
};
}
// Invoke if run directly
const filePath = process.argv[2] || "__FILE_PATH__";
processForm1040(filePath).catch(console.error);import os
import json
import base64
import sys
from extend_ai import Extend
from pydantic import BaseModel, Field
from typing import Optional
client = Extend(token=os.environ["EXTEND_API_KEY"])
class Form1040Data(BaseModel):
tax_year: Optional[str] = Field(
None,
description="Tax year for the return (e.g., 2025). Found in header or form identifier."
)
taxpayer_first_name: Optional[str] = Field(
None,
description="Taxpayer's first name and middle initial. Located in top-left section, before last name field."
)
taxpayer_last_name: Optional[str] = Field(
None,
description="Taxpayer's last name. Located immediately after first name field in header."
)
taxpayer_ssn: Optional[str] = Field(
None,
description="Taxpayer's social security number (9 digits, format XXX-XX-XXXX or no dashes). Critical for IRS matching."
)
spouse_name: Optional[str] = Field(
None,
description="Spouse's first name, middle initial, and last name. Only present if filing jointly or separately; leave null if filing single/HOH."
)
spouse_ssn: Optional[str] = Field(
None,
description="Spouse's social security number (9 digits). Only present if filing status is joint or separately; otherwise null."
)
address: Optional[str] = Field(
None,
description="Home address including street, city, state, and ZIP code. Found in 'Your address' section; format as single line or preserve line breaks."
)
filing_status: Optional[str] = Field(
None,
description="Filing status: must be one of 'Single', 'Married filing jointly', 'Married filing separately', 'Head of household', or 'Qualifying surviving spouse'. Inferred from checkbox marks or text entry."
)
digital_assets: Optional[str] = Field(
None,
description="Whether taxpayer received, sold, or disposed of digital assets (cryptocurrency, NFT, etc.) during tax year. Answer is 'Yes' or 'No'; critical for Schedule D / Form 8949 triggering."
)
presidential_election_campaign: Optional[str] = Field(
None,
description="Whether taxpayer wants $3 to go to presidential election campaign fund. Answer is 'Yes' or 'No' based on checkbox or explicit election."
)
dependents_count: Optional[str] = Field(
None,
description="Number of dependents claimed on return (count of qualifying children/relatives). Extract as string, e.g., '2', '0', or null if blank."
)
us_resident_status: Optional[str] = Field(
None,
description="Whether main home was in U.S. for more than half of tax year. Answer is 'Yes' or 'No'; determines residency test for filing status and dependent qualifications."
)
def process_form1040(file_path: str) -> dict:
"""
Process a Form 1040 from a local file:
1. Parse to markdown using agentic OCR
2. Extract structured tax fields
"""
# Convert local file to base64 data URL for SDK consumption
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}"
print(f"Processing Form 1040 from {file_path}...")
# Step 1: Parse the form to markdown with agentic OCR
print("Step 1: Parsing form with agentic OCR...")
parse_run = client.parse_runs.create_and_poll(
file={"url": data_url},
config={
"blockOptions": {
"text": {
"agentic": {
"enabled": True,
},
},
},
"chunkingStrategy": {
"type": "document",
},
},
)
if parse_run.status != "PROCESSED":
raise Exception(f"Parse failed with status: {parse_run.status}")
# Collect parsed markdown for logging/debugging
parsed_markdown = "\n\n".join(
chunk.get("content", "") for chunk in parse_run.output.get("chunks", [])
)
print(f"✓ Parsed to {len(parse_run.output.get('chunks', []))} chunks")
# Step 2: Extract structured fields using Pydantic schema + review agent
print("Step 2: Extracting Form 1040 fields...")
extract_run = client.extract_runs.create_and_poll(
file={"url": data_url},
config={
"schema": Form1040Data,
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {
"enabled": True,
},
"advancedMultimodalEnabled": True,
},
},
)
if extract_run.status != "PROCESSED":
raise Exception(f"Extraction failed with status: {extract_run.status}")
extracted = extract_run.output.get("value", {})
print("✓ Extraction complete")
# Step 3: Output results
print("\n=== Form 1040 Extraction Results ===\n")
print(json.dumps(extracted, indent=2))
# Return data for programmatic consumption
return {
"parsed_markdown": parsed_markdown,
"extracted_fields": extracted,
"parse_status": parse_run.status,
"extract_status": extract_run.status,
}
if __name__ == "__main__":
file_path = sys.argv[1] if len(sys.argv) > 1 else "__FILE_PATH__"
try:
process_form1040(file_path)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)/*
* This code uses the Extend REST API directly (base URL https://api.extend.ai)
* because Extend does not publish an official Java SDK.
* It calls the same endpoints and uses the same request/response shapes as the TypeScript SDK.
*/
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class Form1040Processor {
private static final String EXTEND_API_BASE = "https://api.extend.ai";
private final HttpClient httpClient;
private final String apiKey;
public Form1040Processor(String apiKey) {
this.apiKey = apiKey;
this.httpClient = HttpClient.newHttpClient();
}
public static void main(String[] args) throws Exception {
String filePath = args.length > 0 ? args[0] : "__FILE_PATH__";
String apiKey = System.getenv("EXTEND_API_KEY");
if (apiKey == null) {
throw new RuntimeException("EXTEND_API_KEY environment variable not set");
}
Form1040Processor processor = new Form1040Processor(apiKey);
try {
processor.processForm1040(filePath);
} catch (Exception e) {
e.printStackTrace();
}
}
public void processForm1040(String filePath) throws Exception {
// Convert local file to base64 data URL
byte[] fileBuffer = Files.readAllBytes(Paths.get(filePath));
String base64 = Base64.getEncoder().encodeToString(fileBuffer);
String dataUrl = "data:application/octet-stream;base64," + base64;
System.out.println("Processing Form 1040 from " + filePath + "...");
// Step 1: Parse the form to markdown with agentic OCR
System.out.println("Step 1: Parsing form with agentic OCR...");
Map<String, Object> parseResponse = createAndPollParseRun(dataUrl);
if (!"PROCESSED".equals(parseResponse.get("status"))) {
throw new RuntimeException("Parse failed with status: " + parseResponse.get("status"));
}
System.out.println("✓ Parsing complete");
// Step 2: Extract structured fields using schema + review agent
System.out.println("Step 2: Extracting Form 1040 fields...");
Map<String, Object> extractResponse = createAndPollExtractRun(dataUrl);
if (!"PROCESSED".equals(extractResponse.get("status"))) {
throw new RuntimeException("Extraction failed with status: " + extractResponse.get("status"));
}
System.out.println("✓ Extraction complete");
// Step 3: Output results
System.out.println("\n=== Form 1040 Extraction Results ===\n");
Map<String, Object> output = (Map<String, Object>) extractResponse.get("output");
if (output != null) {
Map<String, Object> value = (Map<String, Object>) output.get("value");
if (value != null) {
prettyPrintJson(value);
}
}
}
private Map<String, Object> createAndPollParseRun(String dataUrl) throws Exception {
String requestBody = buildParseRequestBody(dataUrl);
HttpRequest request = HttpRequest.newBuilder()
.uri(java.net.URI.create(EXTEND_API_BASE + "/parseRuns"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200 && response.statusCode() != 201) {
throw new RuntimeException("Parse request failed: " + response.statusCode() + " " + response.body());
}
return parseJsonObject(response.body());
}
private Map<String, Object> createAndPollExtractRun(String dataUrl) throws Exception {
String requestBody = buildExtractRequestBody(dataUrl);
HttpRequest request = HttpRequest.newBuilder()
.uri(java.net.URI.create(EXTEND_API_BASE + "/extractRuns"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200 && response.statusCode() != 201) {
throw new RuntimeException("Extract request failed: " + response.statusCode() + " " + response.body());
}
return parseJsonObject(response.body());
}
private String buildParseRequestBody(String dataUrl) {
StringBuilder json = new StringBuilder();
json.append("{");
json.append("\"file\": {\"url\": \"").append(escapeJson(dataUrl)).append("\"},");
json.append("\"config\": {");
json.append("\"blockOptions\": {");
json.append("\"text\": {");
json.append("\"agentic\": {\"enabled\": true}");
json.append("}");
json.append("},");
json.append("\"chunkingStrategy\": {\"type\": \"document\"}");
json.append("}");
json.append("}");
return json.toString();
}
private String buildExtractRequestBody(String dataUrl) {
StringBuilder json = new StringBuilder();
json.append("{");
json.append("\"file\": {\"url\": \"").append(escapeJson(dataUrl)).append("\"},");
json.append("\"config\": {");
json.append("\"schema\": {");
json.append("\"type\": \"object\",");
json.append("\"properties\": {");
json.append("\"tax_year\": {\"type\": [\"string\", \"null\"], \"description\": \"Tax year for the return (e.g., 2025). Found in header or form identifier.\"},");
json.append("\"taxpayer_first_name\": {\"type\": [\"string\", \"null\"], \"description\": \"Taxpayer's first name and middle initial. Located in top-left section, before last name field.\"},");
json.append("\"taxpayer_last_name\": {\"type\": [\"string\", \"null\"], \"description\": \"Taxpayer's last name. Located immediately after first name field in header.\"},");
json.append("\"taxpayer_ssn\": {\"type\": [\"string\", \"null\"], \"description\": \"Taxpayer's social security number (9 digits, format XXX-XX-XXXX or no dashes). Critical for IRS matching.\"},");
json.append("\"spouse_name\": {\"type\": [\"string\", \"null\"], \"description\": \"Spouse's first name, middle initial, and last name. Only present if filing jointly or separately; leave null if filing single/HOH.\"},");
json.append("\"spouse_ssn\": {\"type\": [\"string\", \"null\"], \"description\": \"Spouse's social security number (9 digits). Only present if filing status is joint or separately; otherwise null.\"},");
json.append("\"address\": {\"type\": [\"string\", \"null\"], \"description\": \"Home address including street, city, state, and ZIP code. Found in 'Your address' section; format as single line or preserve line breaks.\"},");
json.append("\"filing_status\": {\"type\": [\"string\", \"null\"], \"description\": \"Filing status: must be one of 'Single', 'Married filing jointly', 'Married filing separately', 'Head of household', or 'Qualifying surviving spouse'. Inferred from checkbox marks or text entry.\"},");
json.append("\"digital_assets\": {\"type\": [\"string\", \"null\"], \"description\": \"Whether taxpayer received, sold, or disposed of digital assets (cryptocurrency, NFT, etc.) during tax year. Answer is 'Yes' or 'No'; critical for Schedule D / Form 8949 triggering.\"},");
json.append("\"presidential_election_campaign\": {\"type\": [\"string\", \"null\"], \"description\": \"Whether taxpayer wants $3 to go to presidential election campaign fund. Answer is 'Yes' or 'No' based on checkbox or explicit election.\"},");
json.append("\"dependents_count\": {\"type\": [\"string\", \"null\"], \"description\": \"Number of dependents claimed on return (count of qualifying children/relatives). Extract as string, e.g., '2', '0', or null if blank.\"},");
json.append("\"us_resident_status\": {\"type\": [\"string\", \"null\"], \"description\": \"Whether main home was in U.S. for more than half of tax year. Answer is 'Yes' or 'No'; determines residency test for filing status and dependent qualifications.\"}");
json.append("}");
json.append("},");
json.append("\"baseProcessor\": \"extraction_performance\",");
json.append("\"advancedOptions\": {");
json.append("\"reviewAgent\": {\"enabled\": true},");
json.append("\"advancedMultimodalEnabled\": true");
json.append("}");
json.append("}");
json.append("}");
return json.toString();
}
private String escapeJson(String str) {
return str.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
}
private Map<String, Object> parseJsonObject(String jsonStr) {
// Simple JSON parser for basic object parsing
Map<String, Object> map = new HashMap<>();
jsonStr = jsonStr.trim();
if (jsonStr.startsWith("{") && jsonStr.endsWith("}")) {
jsonStr = jsonStr.substring(1, jsonStr.length() - 1);
String[] pairs = splitJsonPairs(jsonStr);
for (String pair : pairs) {
int colonIndex = pair.indexOf(':');
if (colonIndex > 0) {
String key = pair.substring(0, colonIndex).trim().replaceAll("\"", "");
String value = pair.substring(colonIndex + 1).trim();
map.put(key, parseJsonValue(value));
}
}
}
return map;
}
private String[] splitJsonPairs(String str) {
java.util.List<String> pairs = new java.util.ArrayList<>();
int depth = 0;
StringBuilder current = new StringBuilder();
boolean inString = false;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == '"' && (i == 0 || str.charAt(i - 1) != '\\')) {
inString = !inString;
}
if (!inString) {
if (c == '{' || c == '[') depth++;
if (c == '}' || c == ']') depth--;
if (c == ',' && depth == 0) {
pairs.add(current.toString());
current = new StringBuilder();
continue;
}
}
current.append(c);
}
if (current.length() > 0) {
pairs.add(current.toString());
}
return pairs.toArray(new String[0]);
}
private Object parseJsonValue(String value) {
value = value.trim();
if (value.equals("true")) return true;
if (value.equals("false")) return false;
if (value.equals("null")) return null;
if (value.startsWith("\"") && value.endsWith("\"")) {
return value.substring(1, value.length() - 1);
}
if (value.startsWith("{")) {
return parseJsonObject(value);
}
if (value.startsWith("[")) {
return parseJsonArray(value);
}
try {
if (value.contains(".")) return Double.parseDouble(value);
return Long.parseLong(value);
} catch (NumberFormatException e) {
return value;
}
}
private Object parseJsonArray(String value) {
java.util.List<Object> list = new java.util.ArrayList<>();
value = value.substring(1, value.length() - 1).trim();
if (value.isEmpty()) return list;
int depth = 0;
StringBuilder current = new StringBuilder();
boolean inString = false;
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (c == '"' && (i == 0 || value.charAt(i - 1) != '\\')) {
inString = !inString;
}
if (!inString) {
if (c == '{' || c == '[') depth++;
if (c == '}' || c == ']') depth--;
if (c == ',' && depth == 0) {
list.add(parseJsonValue(current.toString()));
current = new StringBuilder();
continue;
}
}
current.append(c);
}
if (current.length() > 0) {
list.add(parseJsonValue(current.toString()));
}
return list;
}
private void prettyPrintJson(Map<String, Object> map) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
System.out.println(" \"" + entry.getKey() + "\": " + formatValue(entry.getValue()));
}
}
private String formatValue(Object value) {
if (value == null) return "null";
if (value instanceof String) return "\"" + value + "\"";
if (value instanceof Map) {
StringBuilder sb = new StringBuilder("{\n");
for (Map.Entry<String, Object> e : ((Map<String, Object>) value).entrySet()) {
sb.append(" \"").append(e.getKey()).append("\": ").append(formatValue(e.getValue())).append(",\n");
}
sb.setLength(sb.length() - 2);
sb.append("\n }");
return sb.toString();
}
return value.toString();
}
}// This code calls Extend's REST API directly (base URL https://api.extend.ai)
// because Extend has no official Go SDK yet. The SDK is a thin wrapper over
// this same REST API—we use the same endpoints, request bodies, and response fields.
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
const extendAPIBase = "https://api.extend.ai"
// Form1040Fields represents the extracted tax form fields
type Form1040Fields struct {
TaxYear *string `json:"tax_year"`
TaxpayerFirstName *string `json:"taxpayer_first_name"`
TaxpayerLastName *string `json:"taxpayer_last_name"`
TaxpayerSSN *string `json:"taxpayer_ssn"`
SpouseName *string `json:"spouse_name"`
SpouseSSN *string `json:"spouse_ssn"`
Address *string `json:"address"`
FilingStatus *string `json:"filing_status"`
DigitalAssets *string `json:"digital_assets"`
PresidentialElectionCampaign *string `json:"presidential_election_campaign"`
DependentsCount *string `json:"dependents_count"`
USResidentStatus *string `json:"us_resident_status"`
}
// ProcessForm1040 processes a Form 1040 from a local file:
// 1. Parse to markdown using agentic OCR
// 2. Extract structured tax fields
func ProcessForm1040(filePath string) (map[string]interface{}, error) {
apiKey := os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("EXTEND_API_KEY environment variable not set")
}
// Read file and convert to base64 data URL
fileBuffer, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
base64Str := base64.StdEncoding.EncodeToString(fileBuffer)
dataURL := fmt.Sprintf("data:application/octet-stream;base64,%s", base64Str)
fmt.Printf("Processing Form 1040 from %s...\n", filePath)
// Step 1: Parse the form to markdown with agentic OCR
fmt.Println("Step 1: Parsing form with agentic OCR...")
parseRun, err := createAndPollParseRun(apiKey, dataURL)
if err != nil {
return nil, err
}
if parseRun["status"] != "PROCESSED" {
return nil, fmt.Errorf("parse failed with status: %v", parseRun["status"])
}
// Collect parsed markdown for logging/debugging
output := parseRun["output"].(map[string]interface{})
chunks := output["chunks"].([]interface{})
var parsedMarkdown string
for _, chunk := range chunks {
chunkMap := chunk.(map[string]interface{})
parsedMarkdown += chunkMap["content"].(string) + "\n\n"
}
fmt.Printf("✓ Parsed to %d chunks\n", len(chunks))
// Step 2: Extract structured fields using JSON schema + review agent
fmt.Println("Step 2: Extracting Form 1040 fields...")
extractRun, err := createAndPollExtractRun(apiKey, dataURL)
if err != nil {
return nil, err
}
if extractRun["status"] != "PROCESSED" {
return nil, fmt.Errorf("extraction failed with status: %v", extractRun["status"])
}
extractOutput := extractRun["output"].(map[string]interface{})
extracted := extractOutput["value"].(map[string]interface{})
fmt.Println("✓ Extraction complete")
// Step 3: Output results
fmt.Println("\n=== Form 1040 Extraction Results ===\n")
resultsJSON, _ := json.MarshalIndent(extracted, "", " ")
fmt.Println(string(resultsJSON))
// Return data for programmatic consumption
return map[string]interface{}{
"parsed_markdown": parsedMarkdown,
"extracted_fields": extracted,
"parse_status": parseRun["status"],
"extract_status": extractRun["status"],
}, nil
}
// createAndPollParseRun creates and polls a parse run
func createAndPollParseRun(apiKey, dataURL string) (map[string]interface{}, error) {
client := &http.Client{Timeout: 5 * time.Minute}
parseConfig := map[string]interface{}{
"blockOptions": map[string]interface{}{
"text": map[string]interface{}{
"agentic": map[string]interface{}{
"enabled": true,
},
},
},
"chunkingStrategy": map[string]interface{}{
"type": "document",
},
}
payload := map[string]interface{}{
"file": map[string]interface{}{
"url": dataURL,
},
"config": parseConfig,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", extendAPIBase+"/parse_runs", bytes.NewReader(body))
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("parse request failed: %w", err)
}
defer resp.Body.Close()
var createResp map[string]interface{}
_ = json.NewDecoder(resp.Body).Decode(&createResp)
runID := createResp["id"].(string)
// Poll until status is PROCESSED or error
for {
req, _ := http.NewRequest("GET", extendAPIBase+"/parse_runs/"+runID, nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
resp, _ := client.Do(req)
var pollResp map[string]interface{}
_ = json.NewDecoder(resp.Body).Decode(&pollResp)
resp.Body.Close()
status := pollResp["status"].(string)
if status == "PROCESSED" || status == "FAILED" {
return pollResp, nil
}
time.Sleep(2 * time.Second)
}
}
// createAndPollExtractRun creates and polls an extract run
func createAndPollExtractRun(apiKey, dataURL string) (map[string]interface{}, error) {
client := &http.Client{Timeout: 5 * time.Minute}
schema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"tax_year": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Tax year for the return (e.g., 2025). Found in header or form identifier.",
},
"taxpayer_first_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Taxpayer's first name and middle initial. Located in top-left section, before last name field.",
},
"taxpayer_last_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Taxpayer's last name. Located immediately after first name field in header.",
},
"taxpayer_ssn": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Taxpayer's social security number (9 digits, format XXX-XX-XXXX or no dashes). Critical for IRS matching.",
},
"spouse_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Spouse's first name, middle initial, and last name. Only present if filing jointly or separately; leave null if filing single/HOH.",
},
"spouse_ssn": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Spouse's social security number (9 digits). Only present if filing status is joint or separately; otherwise null.",
},
"address": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Home address including street, city, state, and ZIP code. Found in 'Your address' section; format as single line or preserve line breaks.",
},
"filing_status": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Filing status: must be one of 'Single', 'Married filing jointly', 'Married filing separately', 'Head of household', or 'Qualifying surviving spouse'. Inferred from checkbox marks or text entry.",
},
"digital_assets": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Whether taxpayer received, sold, or disposed of digital assets (cryptocurrency, NFT, etc.) during tax year. Answer is 'Yes' or 'No'; critical for Schedule D / Form 8949 triggering.",
},
"presidential_election_campaign": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Whether taxpayer wants $3 to go to presidential election campaign fund. Answer is 'Yes' or 'No' based on checkbox or explicit election.",
},
"dependents_count": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Number of dependents claimed on return (count of qualifying children/relatives). Extract as string, e.g., '2', '0', or null if blank.",
},
"us_resident_status": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Whether main home was in U.S. for more than half of tax year. Answer is 'Yes' or 'No'; determines residency test for filing status and dependent qualifications.",
},
},
}
extractConfig := map[string]interface{}{
"schema": schema,
"baseProcessor": "extraction_performance",
"advancedOptions": map[string]interface{}{
"reviewAgent": map[string]interface{}{
"enabled": true,
},
"advancedMultimodalEnabled": true,
},
}
payload := map[string]interface{}{
"file": map[string]interface{}{
"url": dataURL,
},
"config": extractConfig,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", extendAPIBase+"/extract_runs", bytes.NewReader(body))
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("extract request failed: %w", err)
}
defer resp.Body.Close()
var createResp map[string]interface{}
_ = json.NewDecoder(resp.Body).Decode(&createResp)
runID := createResp["id"].(string)
// Poll until status is PROCESSED or error
for {
req, _ := http.NewRequest("GET", extendAPIBase+"/extract_runs/"+runID, nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
resp, _ := client.Do(req)
var pollResp map[string]interface{}
_ = json.NewDecoder(resp.Body).Decode(&pollResp)
resp.Body.Close()
status := pollResp["status"].(string)
if status == "PROCESSED" || status == "FAILED" {
return pollResp, nil
}
time.Sleep(2 * time.Second)
}
}
func main() {
filePath := "__FILE_PATH__"
if len(os.Args) > 1 {
filePath = os.Args[1]
}
_, err := ProcessForm1040(filePath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}// Deploy the "Form 1040" 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/tax-return-form-1040.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: tax-return-form-1040).
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, "tax-return-form-1040.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 1040 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": {
"address": {
"type": [
"string",
"null"
],
"description": "Home address including street, city, state, and ZIP code"
},
"tax_year": {
"type": [
"string",
"null"
],
"description": "Tax year for the return (e.g., 2025)"
},
"spouse_ssn": {
"type": [
"string",
"null"
],
"description": "Spouse's social security number"
},
"spouse_name": {
"type": [
"string",
"null"
],
"description": "Spouse's first name, middle initial, and last name"
},
"taxpayer_ssn": {
"type": [
"string",
"null"
],
"description": "Taxpayer's social security number"
},
"filing_status": {
"type": [
"string",
"null"
],
"description": "Filing status (Single, Married filing jointly, Married filing separately, Head of household, or Qualifying surviving spouse)"
},
"digital_assets": {
"type": [
"string",
"null"
],
"description": "Whether taxpayer received, sold, or disposed of digital assets during tax year (Yes/No)"
},
"dependents_count": {
"type": [
"string",
"null"
],
"description": "Number of dependents claimed on return"
},
"taxpayer_last_name": {
"type": [
"string",
"null"
],
"description": "Taxpayer's last name"
},
"us_resident_status": {
"type": [
"string",
"null"
],
"description": "Whether main home was in U.S. for more than half of tax year"
},
"taxpayer_first_name": {
"type": [
"string",
"null"
],
"description": "Taxpayer's first name and middle initial"
},
"presidential_election_campaign": {
"type": [
"string",
"null"
],
"description": "Whether taxpayer wants $3 to go to presidential election campaign fund"
}
}
},
"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 json
import os
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.")
sys.exit(1)
STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "tax-return-form-1040.json"
def load_state() -> dict:
if STATE_FILE.exists():
with open(STATE_FILE, "r") as f:
return json.load(f)
return {}
def save_state(state: dict) -> None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
with open(STATE_FILE, "w") as f:
json.dump(state, f, indent=2)
WORKFLOW = {
"name": "Form 1040 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": {
"address": {
"type": ["string", "null"],
"description": "Home address including street, city, state, and ZIP code",
},
"tax_year": {
"type": ["string", "null"],
"description": "Tax year for the return (e.g., 2025)",
},
"spouse_ssn": {
"type": ["string", "null"],
"description": "Spouse's social security number",
},
"spouse_name": {
"type": ["string", "null"],
"description": "Spouse's first name, middle initial, and last name",
},
"taxpayer_ssn": {
"type": ["string", "null"],
"description": "Taxpayer's social security number",
},
"filing_status": {
"type": ["string", "null"],
"description": "Filing status (Single, Married filing jointly, Married filing separately, Head of household, or Qualifying surviving spouse)",
},
"digital_assets": {
"type": ["string", "null"],
"description": "Whether taxpayer received, sold, or disposed of digital assets during tax year (Yes/No)",
},
"dependents_count": {
"type": ["string", "null"],
"description": "Number of dependents claimed on return",
},
"taxpayer_last_name": {
"type": ["string", "null"],
"description": "Taxpayer's last name",
},
"us_resident_status": {
"type": ["string", "null"],
"description": "Whether main home was in U.S. for more than half of tax year",
},
"taxpayer_first_name": {
"type": ["string", "null"],
"description": "Taxpayer's first name and middle initial",
},
"presidential_election_campaign": {
"type": ["string", "null"],
"description": "Whether taxpayer wants $3 to go to presidential election campaign fund",
},
},
},
"baseProcessor": "extraction_performance",
"advancedOptions": {
"reviewAgent": {"enabled": True},
"advancedMultimodalEnabled": True,
},
}
},
},
],
}
def main():
client = Extend(token=API_KEY)
state = load_state()
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_resp = client.workflows.list(name=WORKFLOW["name"])
items = getattr(workflows_resp, "data", None) or getattr(
workflows_resp, "items", None
) or []
for item in items:
if getattr(item, "name", None) == WORKFLOW["name"]:
existing_id = getattr(item, "id", None)
break
except Exception:
pass
if existing_id:
state["workflowId"] = existing_id
save_state(state)
print(
f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing_id}) — updating steps'
)
client.workflows.update(id=existing_id, steps=WORKFLOW["steps"])
else:
created = client.workflows.create(**WORKFLOW)
workflow_id = getattr(created, "id", None) or getattr(
getattr(created, "workflow", None), "id", None
)
if not workflow_id:
raise Exception("Could not read created workflow id from response")
state["workflowId"] = workflow_id
save_state(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)// Extend REST API provisioning script for "Form 1040" template.
// Uses Extend's REST API directly (https://api.extend.ai) via Java's built-in HttpClient.
// Extend does not publish an official Java SDK; this code calls the API endpoints directly.
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.HashMap;
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 HttpClient HTTP = HttpClient.newHttpClient();
private static final Path STATE_DIR = Paths.get(System.getProperty("user.dir"), ".extend");
private static final Path STATE_FILE = STATE_DIR.resolve("tax-return-form-1040.json");
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 \"" + (String) workflow.get("name") + "\"…");
if (state.workflowId != null && !state.workflowId.isEmpty()) {
System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
Map<String, Object> updateBody = new HashMap<>();
updateBody.put("steps", workflow.get("steps"));
api("POST", "/workflows/" + state.workflowId, updateBody);
} else {
String workflowName = (String) workflow.get("name");
try {
String listPath = "/workflows?name=" + URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
Map<String, Object> list = api("GET", listPath, null);
List<Map<String, Object>> items = (List<Map<String, Object>>) (list.getOrDefault("data", list.getOrDefault("items", List.of())));
for (Map<String, Object> item : items) {
if (workflowName.equals(item.get("name"))) {
state.workflowId = (String) item.get("id");
saveState();
System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + state.workflowId + ") — updating steps");
Map<String, Object> updateBody = new HashMap<>();
updateBody.put("steps", workflow.get("steps"));
api("POST", "/workflows/" + state.workflowId, updateBody);
break;
}
}
} catch (Exception e) {
// lookup is best-effort; fall through to create
}
if (state.workflowId == null || state.workflowId.isEmpty()) {
Map<String, Object> created = api("POST", "/workflows", workflow);
String wfId = (String) created.getOrDefault("id", null);
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 + ")");
}
}
// Deploy the current draft as a new version (best-effort).
try {
api("POST", "/workflows/" + state.workflowId + "/versions", new HashMap<>());
} catch (Exception e) {
// best-effort; some accounts may not require this
}
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() {
if (Files.exists(STATE_FILE)) {
try {
String content = Files.readString(STATE_FILE);
Map<String, Object> parsed = parseJson(content);
state.workflowId = (String) parsed.get("workflowId");
} catch (IOException e) {
// ignore, state remains empty
}
}
}
private static void saveState() throws IOException {
Files.createDirectories(STATE_DIR);
Map<String, Object> toSave = new HashMap<>();
if (state.workflowId != null) {
toSave.put("workflowId", state.workflowId);
}
Files.writeString(STATE_FILE, toJsonString(toSave));
}
private static Map<String, Object> api(String method, String pathName, Map<String, Object> body)
throws IOException, InterruptedException {
String url = API + pathName;
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url))
.method(method, body != null ? HttpRequest.BodyPublishers.ofString(toJsonString(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 = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> data = new HashMap<>();
try {
data = parseJson(response.body());
} catch (Exception e) {
// empty map on parse failure
}
if (response.statusCode() < 200 || response.statusCode() >= 300) {
String errorMsg = toJsonString(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 1040 Processing Pipeline");
// Build steps array
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 HashMap<>();
Map<String, Object> parseConfig = new HashMap<>();
Map<String, Object> blockOptions = new HashMap<>();
Map<String, Object> textOptions = new HashMap<>();
Map<String, Object> agenticOptions = new HashMap<>();
agenticOptions.put("enabled", true);
textOptions.put("agentic", agenticOptions);
blockOptions.put("text", textOptions);
parseConfig.put("blockOptions", blockOptions);
Map<String, Object> chunkingStrategy = new HashMap<>();
chunkingStrategy.put("type", "document");
parseConfig.put("chunkingStrategy", chunkingStrategy);
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 HashMap<>();
Map<String, Object> extractorConfig = new HashMap<>();
Map<String, Object> schema = new HashMap<>();
schema.put("type", "object");
Map<String, Object> properties = new LinkedHashMap<>();
properties.put("address", buildProperty("Home address including street, city, state, and ZIP code"));
properties.put("tax_year", buildProperty("Tax year for the return (e.g., 2025)"));
properties.put("spouse_ssn", buildProperty("Spouse's social security number"));
properties.put("spouse_name", buildProperty("Spouse's first name, middle initial, and last name"));
properties.put("taxpayer_ssn", buildProperty("Taxpayer's social security number"));
properties.put("filing_status", buildProperty("Filing status (Single, Married filing jointly, Married filing separately, Head of household, or Qualifying surviving spouse)"));
properties.put("digital_assets", buildProperty("Whether taxpayer received, sold, or disposed of digital assets during tax year (Yes/No)"));
properties.put("dependents_count", buildProperty("Number of dependents claimed on return"));
properties.put("taxpayer_last_name", buildProperty("Taxpayer's last name"));
properties.put("us_resident_status", buildProperty("Whether main home was in U.S. for more than half of tax year"));
properties.put("taxpayer_first_name", buildProperty("Taxpayer's first name and middle initial"));
properties.put("presidential_election_campaign", buildProperty("Whether taxpayer wants $3 to go to presidential election campaign fund"));
schema.put("properties", properties);
extractorConfig.put("schema", schema);
extractorConfig.put("baseProcessor", "extraction_performance");
Map<String, Object> advancedOptions = new HashMap<>();
Map<String, Object> reviewAgent = new HashMap<>();
reviewAgent.put("enabled", true);
advancedOptions.put("reviewAgent", reviewAgent);
advancedOptions.put("advancedMultimodalEnabled", true);
extractorConfig.put("advancedOptions", advancedOptions);
config.put("extractorConfig", extractorConfig);
step.put("config", config);
return step;
}
private static Map<String, Object> buildProperty(String description) {
Map<String, Object> prop = new LinkedHashMap<>();
prop.put("type", List.of("string", "null"));
prop.put("description", description);
return prop;
}
private static String toJsonString(Map<String, Object> map) {
return serializeJson(map);
}
private static String serializeJson(Object obj) {
if (obj == null) {
return "null";
}
if (obj instanceof String) {
return "\"" + escapeString((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(escapeString(entry.getKey())).append("\":");
sb.append(serializeJson(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(serializeJson(item));
first = false;
}
sb.append("]");
return sb.toString();
}
return "null";
}
private static String escapeString(String s) {
return s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
private static Map<String, Object> parseJson(String json) {
// Simple JSON parser for response handling
json = json.trim();
if (!json.startsWith("{")) {
return new HashMap<>();
}
Map<String, Object> result = new HashMap<>();
int depth = 0;
int start = 1;
String key = null;
boolean inString = false;
boolean escaped = false;
for (int i = 1; i < json.length() - 1; i++) {
char c = json.charAt(i);
if (escaped) {
escaped = false;
continue;
}
if (c == '\\') {
escaped = true;
continue;
}
if (c == '"') {
inString = !inString;
continue;
}
if (inString) {
continue;
}
if (c == '{' || c == '[') {
depth++;
} else if (c == '}' || c == ']') {
depth--;
} else if (c == ':' && depth == 0) {
key = json.substring(start, i).trim();
if (key.startsWith("\"") && key.endsWith("\"")) {
key = key.substring(1, key.length() - 1);
}
start = i + 1;
} else if ((c == ',' || i == json.length() - 2) && depth == 0) {
int end = (c == ',') ? i : json.length() - 1;
String value = json.substring(start, end).trim();
if (key != null) {
result.put(key, parseValue(value));
key = null;
}
start = i + 1;
}
}
return result;
}
private static Object parseValue(String value) {
if (value == null || value.isEmpty() || "null".equals(value)) {
return null;
}
if ("true".equals(value)) {
return true;
}
if ("false".equals(value)) {
return false;
}
if (value.startsWith("\"") && value.endsWith("\"")) {
return value.substring(1, value.length() - 1);
}
try {
if (value.contains(".")) {
return Double.parseDouble(value);
}
return Long.parseLong(value);
} catch (NumberFormatException e) {
return value;
}
}
}// This code calls the Extend REST API directly because Extend has no official Go SDK yet.
// It deploys the "Form 1040" 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: tax-return-form-1040).
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"
)
type State struct {
WorkflowID string `json:"workflowId,omitempty"`
}
func main() {
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, "tax-return-form-1040.json")
state := State{}
if data, err := os.ReadFile(stateFile); err == nil {
json.Unmarshal(data, &state)
}
saveState := func() error {
if err := os.MkdirAll(stateDir, 0755); err != nil {
return err
}
data, _ := json.MarshalIndent(state, "", " ")
return os.WriteFile(stateFile, data, 0644)
}
apiCall := func(method, pathName string, body interface{}) (map[string]interface{}, error) {
var bodyReader io.Reader
headers := make(map[string]string)
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["x-extend-api-version"] = VERSION
if body != nil {
bodyBytes, _ := json.Marshal(body)
bodyReader = bytes.NewReader(bodyBytes)
headers["Content-Type"] = "application/json"
}
req, _ := http.NewRequest(method, API+pathName, bodyReader)
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var data map[string]interface{}
json.Unmarshal(respBody, &data)
if resp.StatusCode >= 400 {
preview := string(respBody)
if len(preview) > 300 {
preview = preview[:300]
}
return nil, fmt.Errorf("%s %s failed (%d): %s", method, pathName, resp.StatusCode, preview)
}
return data, nil
}
workflow := map[string]interface{}{
"name": "Form 1040 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{}{
"address": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Home address including street, city, state, and ZIP code",
},
"tax_year": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Tax year for the return (e.g., 2025)",
},
"spouse_ssn": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Spouse's social security number",
},
"spouse_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Spouse's first name, middle initial, and last name",
},
"taxpayer_ssn": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Taxpayer's social security number",
},
"filing_status": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Filing status (Single, Married filing jointly, Married filing separately, Head of household, or Qualifying surviving spouse)",
},
"digital_assets": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Whether taxpayer received, sold, or disposed of digital assets during tax year (Yes/No)",
},
"dependents_count": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Number of dependents claimed on return",
},
"taxpayer_last_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Taxpayer's last name",
},
"us_resident_status": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Whether main home was in U.S. for more than half of tax year",
},
"taxpayer_first_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Taxpayer's first name and middle initial",
},
"presidential_election_campaign": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Whether taxpayer wants $3 to go to presidential election campaign fund",
},
},
},
"baseProcessor": "extraction_performance",
"advancedOptions": map[string]interface{}{
"reviewAgent": map[string]interface{}{
"enabled": true,
},
"advancedMultimodalEnabled": true,
},
},
},
},
},
}
fmt.Printf("Deploying \"%s\"…\n", workflow["name"])
if state.WorkflowID != "" {
fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
_, _ = apiCall("POST", fmt.Sprintf("/workflows/%s", state.WorkflowID), map[string]interface{}{
"steps": workflow["steps"],
})
} else {
found := false
if listResp, err := apiCall("GET", fmt.Sprintf("/workflows?name=%s", url.QueryEscape(workflow["name"].(string))), nil); 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 data, ok := listResp["items"].([]interface{}); ok {
for _, item := range data {
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"].(string) {
if id, ok := item["id"].(string); ok {
state.WorkflowID = id
saveState()
fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflow["name"], id)
_, _ = apiCall("POST", fmt.Sprintf("/workflows/%s", id), map[string]interface{}{
"steps": workflow["steps"],
})
found = true
break
}
}
}
}
if !found {
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 workflowData, ok := created["workflow"].(map[string]interface{}); ok {
if id, ok := workflowData["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)
}
}
_, _ = 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.")
}Form 1040 is the primary U.S. individual income tax return filed annually with the IRS. This template captures taxpayer identification, filing status, dependent information, and digital asset disclosures. It processes personal details, addresses, and tax election preferences required for federal income tax compliance.