# config import { TypeTable } from 'fumadocs-ui/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; Synopsis [#synopsis] ```bash struktur config providers [options] struktur config models [options] struktur config parsers [options] ``` Configuration is stored at `~/.config/struktur/config.json`. Override the directory with `STRUKTUR_CONFIG_DIR`. *** config providers [#config-providers] Manage API tokens for LLM providers. config providers list [#config-providers-list] List all supported providers with their configuration status. ```bash struktur config providers list ``` Output: ```json [ { "provider": "openai", "configured": true, "storage": "keychain" }, { "provider": "anthropic", "configured": false, "storage": null }, { "provider": "google", "configured": false, "storage": null }, { "provider": "opencode", "configured": false, "storage": null }, { "provider": "openrouter", "configured": false, "storage": null } ] ``` config providers add [#config-providers-add] Store an API token for a provider. ```bash echo "$OPENAI_API_KEY" | struktur config providers add openai --token-stdin ``` ```bash echo "$OPENAI_API_KEY" | struktur config providers add openai --token-stdin --default ``` ```bash echo "$OPENCODE_API_KEY" | struktur config providers add opencode --token-stdin --default ``` ```bash echo "$OPENROUTER_API_KEY" | struktur config providers add openrouter --token-stdin ``` Output (without `--default`): `{ "provider": "openai", "stored": "keychain" }` Output (with `--default`): `{ "provider": "openai", "stored": "keychain", "defaultModel": "openai/gpt-4.1-nano" }` **Storage:** On macOS, tokens go into Keychain by default. On other platforms, `~/.config/struktur/tokens.json` (chmod 600). Set `STRUKTUR_DISABLE_KEYCHAIN` to force file storage on macOS. config providers remove [#config-providers-remove] Delete a stored token. ```bash struktur config providers remove ``` Output: `{ "provider": "openai", "deleted": true }` *** config models [#config-models] Manage the default model and model aliases. config models list [#config-models-list] List available models for a provider (queries the provider API). ```bash struktur config models list [--provider ] ``` config models use [#config-models-use] Set the default model. Accepts a `provider/model` spec or a stored alias. ```bash struktur config models use openai/gpt-4o-mini struktur config models use fast # if alias "fast" is defined ``` The alias is resolved before storing, so config always holds a real model string. config models alias [#config-models-alias] Manage short aliases for model specs. Aliases resolve transparently everywhere a model is accepted — `--model fast` works identically to `--model openai/gpt-4.1-mini`. ```bash # List all aliases struktur config models alias list # Get a specific alias struktur config models alias get fast # Set an alias struktur config models alias set fast openai/gpt-4.1-mini struktur config models alias set smart anthropic/claude-3-5-haiku-20241022 # Remove an alias struktur config models alias remove fast ``` `alias list` output: ```json { "aliases": { "fast": "openai/gpt-4.1-mini", "smart": "anthropic/claude-3-5-haiku-20241022" } } ``` *** config parsers [#config-parsers] Register custom parsers for file formats not supported natively. Parsers are keyed by MIME type. config parsers list [#config-parsers-list] ```bash struktur config parsers list ``` config parsers get [#config-parsers-get] ```bash struktur config parsers get --mime application/vnd.ms-excel ``` config parsers add [#config-parsers-add] Exactly one of `--npm`, `--file-command`, or `--stdin-command` must be specified. **npm parser** — install a package that implements the [npm parser contract](/docs/explanation/document-parsing#npm-package-parser): ```bash struktur config parsers add \ --mime application/vnd.openxmlformats-officedocument.wordprocessingml.document \ --npm @myorg/docx-parser ``` **file-command** — shell command, `FILE_PATH` is replaced with the actual path: ```bash struktur config parsers add \ --mime application/vnd.openxmlformats-officedocument.wordprocessingml.document \ --file-command "markitdown FILE_PATH" ``` `FILE_PATH` must appear in the command string — an error is thrown if it is missing. The command must write `SerializedArtifact[]` JSON to stdout. **stdin-command** — file contents are piped to stdin: ```bash struktur config parsers add \ --mime text/html \ --stdin-command "my-html-to-artifact-converter" ``` The command must write `SerializedArtifact[]` JSON to stdout. Plain text output will fail validation. config parsers remove [#config-parsers-remove] ```bash struktur config parsers remove --mime application/vnd.ms-excel ``` *** OpenRouter provider routing [#openrouter-provider-routing] When using OpenRouter, you can specify a preferred inference provider using the `#` syntax in the model spec: ```bash # Use Claude 3.5 Sonnet via Cerebras for faster inference struktur --input doc.pdf --model "openrouter/anthropic/claude-3.5-sonnet#cerebras" --fields "..." # Use Claude via Together AI struktur --input doc.pdf --model "openrouter/anthropic/claude-3.5-sonnet#together" --fields "..." ``` This is passed through to OpenRouter's provider routing feature. See also [#see-also] * [Installation & Setup](/docs/cli/installation) — initial setup and environment variables * [Document Parsing](/docs/explanation/document-parsing) — how the parser system works # extract import { TypeTable } from 'fumadocs-ui/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; Synopsis [#synopsis] ```bash struktur [extract] [options] ``` `extract` is the default command — `struktur --input file.pdf ...` and `struktur extract --input file.pdf ...` are equivalent. Input options (exactly one required) [#input-options-exactly-one-required] Schema options (exactly one required) [#schema-options-exactly-one-required] `--fields` is the quickest way to define a schema without writing JSON. See [--fields reference](/docs/cli/fields) for the full syntax. Model [#model] Supported providers: `openai`, `anthropic`, `google`, `opencode`, `openrouter`. For OpenRouter, you can specify a preferred inference provider using `#` syntax: ```bash --model "openrouter/anthropic/claude-3.5-sonnet#cerebras" ``` Parsing options [#parsing-options] These flags control how `--input` files are parsed before extraction. Image options (PDF inputs) [#image-options-pdf-inputs] For custom screenshot dimensions, use `struktur parse --screenshots --screenshot-scale ` and pipe the artifact to `struktur extract --artifact-file -`. Strategy [#strategy] Strategy names: `simple`, `parallel`, `sequential`, `parallelAutoMerge`, `sequentialAutoMerge`, `doublePass`, `doublePassAutoMerge`. When using `--strategy` other than `simple`, both `model` and `mergeModel`/`dedupeModel` are set to the same model. For different models per role, use the TypeScript SDK. Output [#output] Progress [#progress] When stderr is a TTY, a progress bar is shown: ``` ◈ ▰▰▰▰▰▱▱▱▱▱ 50% | batch 2/5 ``` The bar is suppressed in non-interactive mode (piped stderr). Examples [#examples] ```bash echo "Invoice #1042 from Acme Corp. Total: $2,400.00." | \ struktur --stdin -f "invoice_number, vendor, total:number" \ --model openai/gpt-4o-mini ``` ```bash struktur --input invoice.pdf \ --fields "invoice_number, vendor, total:number" \ --model openai/gpt-4o-mini ``` ```bash struktur --input invoice.pdf --images \ --schema invoice-schema.json \ --model openai/gpt-4o ``` ```bash # Use parse for custom screenshot settings, then pipe to extract struktur parse --input slides.pdf --screenshots --screenshot-scale 2 | \ struktur --artifact-file - \ --fields "title, slide_count:integer" \ --model openai/gpt-4o ``` ```bash struktur --input report.txt \ --schema-json '{"type":"object","properties":{"summary":{"type":"string"}},"required":["summary"],"additionalProperties":false}' \ --model openai/gpt-4o-mini ``` ```bash cat document.md | struktur --stdin --schema schema.json --model anthropic/claude-3-5-haiku-20241022 ``` ```bash struktur --input large.md --schema schema.json --model openai/gpt-4o \ --strategy parallel --output result.json ``` ```bash struktur --input data.bin --mime application/pdf \ --fields "title, author" --model openai/gpt-4o-mini ``` ```bash struktur --input report.docx --parser @myorg/docx-parser \ --fields "title, summary" --model openai/gpt-4o-mini ``` ```bash struktur --input data.txt --schema https://myserver.com/schemas/invoice.json --model openai/gpt-4o-mini ``` ```bash struktur --input doc.pdf --fields "title" --model openai/gpt-4o-mini --debug ``` See also [#see-also] * [--fields reference](/docs/cli/fields) — fields shorthand syntax and examples * [parse](/docs/cli/parse) — convert files to artifact JSON for inspection * [config](/docs/cli/config) — provider and model management * [Document Parsing](/docs/explanation/document-parsing) — how file parsing works * [Strategies](/docs/explanation/strategies) — strategy reference # Fields Shorthand import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; import { TypeTable } from 'fumadocs-ui/components/type-table'; import { Card, Cards } from 'fumadocs-ui/components/card'; The `--fields` flag (short: `-f`) lets you describe extraction output as a comma-separated string directly on the command line, without writing or maintaining a JSON Schema file. ```bash echo "The Dark Knight (2008), directed by Christopher Nolan. Genre: action." | \ struktur --stdin --fields "title, year:integer, director, genre" \ --model openai/gpt-4o-mini ``` Output: ```json { "title": "The Dark Knight", "year": 2008, "director": "Christopher Nolan", "genre": "action" } ``` *** Synopsis [#synopsis] ```bash struktur [extract] --fields "" [other options] ``` `--fields` is one of three mutually exclusive schema options. Pass exactly one of: *** Field syntax [#field-syntax] ``` fields = field ("," field)* field = name | name ":" type ``` Whitespace around commas and colons is ignored. *** Scalar types [#scalar-types] ```bash --fields "title" --fields "title:string" ``` Default when no type is specified. Produces `{ "type": "string" }`. ```bash --fields "price:number" --fields "price:float" ``` Any numeric value. Both produce `{ "type": "number" }`. `float` is an alias for `number`. ```bash --fields "count:integer" --fields "count:int" ``` Whole numbers only. `integer` produces `{ "type": "integer" }`. `int` produces `{ "type": "integer", "multipleOf": 1 }` — explicitly disallows fractions. ```bash --fields "active:boolean" --fields "active:bool" ``` Both produce `{ "type": "boolean" }`. `bool` is an alias for `boolean`. *** Enums [#enums] ```bash --fields "status:enum{draft|published|archived}" ``` Values are separated by `|`. At least two values are required. *** Arrays [#arrays] ```bash --fields "tags:array" # shorthand for array{string} --fields "tags:array{string}" --fields "scores:array{float}" --fields "ids:array{int}" ``` The item type can be any scalar keyword (including aliases). If omitted, defaults to `string`. *** Examples [#examples] ```bash struktur --input article.txt \ --fields "title, author, published_date, word_count:integer" \ --model openai/gpt-4o-mini ``` ```bash echo "Order #4421 is currently being packed." | \ struktur --stdin \ --fields "order_id, status:enum{pending|processing|shipped|delivered}" \ --model anthropic/claude-3-5-haiku-20241022 ``` ```bash struktur --input product.html \ --fields "name, price:float, in_stock:bool, tags:array{string}, category:enum{electronics|clothing|food}" \ --model openai/gpt-4o-mini ``` ```bash cat reviews.txt | \ struktur --stdin \ --fields "sentiment:enum{positive|neutral|negative}, score:int, summary" \ --model openai/gpt-4o-mini \ --output result.json ``` ```bash for f in docs/*.txt; do struktur --input "$f" \ --fields "title, category:enum{invoice|receipt|contract}, amount:float" \ --model openai/gpt-4o-mini \ --output "out/$(basename "$f" .txt).json" done ``` *** Generated schema [#generated-schema] `--fields "title, price:number, tags:array"` produces this schema internally: ```json { "type": "object", "properties": { "title": { "type": "string" }, "price": { "type": "number" }, "tags": { "type": "array", "items": { "type": "string" } } }, "required": ["title", "price", "tags"], "additionalProperties": false } ``` All fields are required. For optional fields, nested objects, or `$ref`, use `--schema` instead. *** SDK Usage [#sdk-usage] The `fields` parameter is also available in the SDK: ```ts import { extract, simple } from "@struktur/sdk"; import { openai } from "@ai-sdk/openai"; const result = await extract({ artifacts, fields: "title, author, year:integer, genre:enum{fiction|nonfiction|reference}", strategy: simple({ model: openai("gpt-4o-mini") }), }); // result.data is typed as Record when using fields console.log(result.data.title); ``` For full TypeScript inference on `result.data`, use `schema` with `JSONSchemaType` instead. Utility exports [#utility-exports] The parser and schema builder are exported for use outside of `extract()`: ```ts import { parseFieldsString, buildSchemaFromParsedFields, buildSchemaFromFields, } from "@struktur/sdk"; // Parse to an intermediate representation const parsed = parseFieldsString("title, price:number, status:enum{draft|live}"); // [ // { name: "title", kind: "scalar", type: "string" }, // { name: "price", kind: "scalar", type: "number" }, // { name: "status", kind: "enum", values: ["draft", "live"] } // ] // Build a schema directly const schema = buildSchemaFromFields("title, price:number"); ``` parseFieldsString(fields: string): ParsedField[] [#parsefieldsstringfields-string-parsedfield] Parses the fields string into an array of `ParsedField` discriminated union entries: ```ts type ParsedField = | { name: string; kind: "scalar"; type: ScalarFieldType } | { name: string; kind: "enum"; values: string[] } | { name: string; kind: "array"; items: ScalarFieldType }; type ScalarFieldType = "string" | "number" | "boolean" | "integer" | "int"; ``` buildSchemaFromParsedFields(fields: ParsedField[]): AnyJSONSchema [#buildschemafromparsedfieldsfields-parsedfield-anyjsonschema] Builds the JSON Schema object from a pre-parsed array. Useful if you want to inspect or modify the parsed fields before building. buildSchemaFromFields(fields: string): AnyJSONSchema [#buildschemafromfieldsfields-string-anyjsonschema] Convenience one-liner: parses and builds in a single call. *** Error messages [#error-messages] Bad field definitions fail immediately with a helpful message: ```bash --fields "count:bigint" # Error: Unknown type "bigint" for field "count". # Scalar types: bool, boolean, float, int, integer, number, string. # Complex types: enum{a|b|c}, array{string}, or array (shorthand for array{string}). --fields "role:enum{admin}" # Error: enum for field "role" must have at least two values separated by "|", got: "admin". --fields "tags:array{}" # Error: array for field "tags" requires an item type, e.g. array{string}. --fields "name:enum{a|b" # Error: Unmatched braces in fields string. ``` *** When to use --fields vs --schema [#when-to-use---fields-vs---schema] | Situation | Use | | ------------------------------------------ | ----------------------------------- | | Quick one-liner or experiment | `--fields` | | All fields are flat, all required | `--fields` | | Need optional properties or nested objects | `--schema` | | Schema is reused across many runs | `--schema` | | Need `$ref`, `allOf`, custom formats | `--schema` | | Need TypeScript inference on `result.data` | `--schema` with `JSONSchemaType` | *** See also [#see-also] * [extract](/docs/cli/extract) — full CLI flag reference # CLI import { Card, Cards } from 'fumadocs-ui/components/card'; The Struktur CLI provides commands for data extraction, file parsing, and configuration management. `extract` is the default command — `struktur --input file.pdf ...` and `struktur extract --input file.pdf ...` are equivalent. # Installation & Setup import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { Callout } from 'fumadocs-ui/components/callout'; Install [#install] ```bash npm install -g @struktur/cli ``` ```bash bun install -g @struktur/cli ``` Verify: ```bash struktur --help ``` Configure a provider (required) [#configure-a-provider-required] Store your API key securely with the CLI: ```bash echo "$OPENAI_API_KEY" | struktur config providers add openai --token-stdin ``` On macOS, tokens are stored in Keychain. On other platforms, `~/.config/struktur/tokens.json` (chmod 600). Quick setup with --default [#quick-setup-with---default] ```bash echo "$OPENAI_API_KEY" | struktur config providers add openai --token-stdin --default ``` The `--default` flag automatically queries the provider API and sets the cheapest available model as default. One command, fully ready. Set a default model [#set-a-default-model] ```bash # Set explicitly struktur config models use openai/gpt-4o-mini # Or store a shortcut alias first struktur config models alias set fast openai/gpt-4.1-mini struktur config models use fast ``` Once set, `--model` is optional in `extract` commands. Environment variables [#environment-variables] Provider API keys can also be set via environment variables. This is useful for CI/CD or temporary sessions, but stored tokens are recommended for regular use. Provider API keys [#provider-api-keys] | Variable | Provider | | ------------------------------ | ---------- | | `OPENAI_API_KEY` | OpenAI | | `ANTHROPIC_API_KEY` | Anthropic | | `GOOGLE_GENERATIVE_AI_API_KEY` | Google | | `OPENCODE_API_KEY` | OpenCode | | `OPENROUTER_API_KEY` | OpenRouter | Environment variables override stored tokens. Configuration [#configuration] | Variable | Purpose | | --------------------------- | --------------------------------------------------------- | | `STRUKTUR_CONFIG_DIR` | Override config directory (default: `~/.config/struktur`) | | `STRUKTUR_DISABLE_KEYCHAIN` | Set to any value to disable macOS Keychain | | `STRUKTUR_KEYCHAIN_SERVICE` | Override Keychain service name | SDK behavior [#sdk-behavior] | Variable | Purpose | | --------------------- | ----------------------------------------------- | | `AI_SDK_LOG_WARNINGS` | Set to `true` to enable AI SDK warning messages | See also [#see-also] * [extract](/docs/cli/extract) — main extraction command * [config](/docs/cli/config) — provider and model management # parse import { TypeTable } from 'fumadocs-ui/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; import { Card, Cards } from 'fumadocs-ui/components/card'; Synopsis [#synopsis] ```bash struktur parse --input [options] struktur parse --stdin [options] ``` Description [#description] Converts a file or stdin to Artifact JSON. Use this to: Options [#options] Input (exactly one required) [#input-exactly-one-required] Output [#output] Parser control [#parser-control] Image extraction (PDF inputs) [#image-extraction-pdf-inputs] Parser resolution order [#parser-resolution-order] 1. `--parser ` flag — bypasses all config 2. Parser configured for the detected MIME type (`struktur config parsers add ...`) 3. Built-in parser for the MIME type 4. Error: no parser found — suggests `struktur config parsers add` Built-in parsers [#built-in-parsers] | MIME type | Behavior | | ------------------ | ---------------------------------------------------------------------------------------------------- | | `application/pdf` | Per-page text via `pdf-parse`. Add `--images` for embedded images, `--screenshots` for page renders. | | `text/*` | Split on double newlines into content slices. | | `image/*` | Single-content artifact with the image as a media item. | | `application/json` | If it validates as `SerializedArtifact[]`, passed through unchanged. | Examples [#examples] ```bash struktur parse --input document.pdf ``` ```bash struktur parse --input slides.pdf --images --screenshots --output artifact.json ``` ```bash struktur parse --input data.xlsx --parser @myorg/xlsx-parser ``` ```bash struktur parse --input doc.pdf --images | \ struktur extract --artifact-file - --fields "title, author" --model openai/gpt-4o-mini ``` ```bash struktur parse --input doc.pdf | struktur utils artifact-viewer --stdin > viewer.html open viewer.html ``` See also [#see-also] * [config parsers](/docs/cli/config#config-parsers) — Configure custom parsers * [Document Parsing](/docs/explanation/document-parsing) — Parser system overview * [Artifact Format](/docs/explanation/artifact-format) — Output format * [utils artifact-viewer](/docs/cli/utils) — Visualize parsed artifacts # utils import { TypeTable } from 'fumadocs-ui/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; utils artifact-viewer [#utils-artifact-viewer] Generates a self-contained HTML file for exploring artifact JSON in a browser. ```bash struktur utils artifact-viewer --input artifacts.json --output viewer.html struktur parse --input doc.pdf --images | struktur utils artifact-viewer --stdin > viewer.html ``` Options [#options] What the viewer shows [#what-the-viewer-shows] **Default view features:** * Each artifact as a card with header showing type, page count, and image count * Text content with expand/collapse per-slice (truncated at 500 chars, full text on click) * Image thumbnails with click-to-enlarge modal * Screenshot images marked with an orange "screenshot" badge * Image dimensions overlaid on each thumbnail * Metadata section (collapsible) **Batching Mode features:** * Sidebar listing batches and chunks with token and image counts * Main area shows each chunk with a dashed amber border at chunk boundaries * Configurable chunking parameters: Max Tokens, Max Images, Text Ratio, Image Tokens * Image type filter: show/hide embedded images and screenshots independently * Token and image counts update live as parameters change The viewer embeds a JavaScript implementation of Struktur's chunking algorithm so batching mode accurately reflects what `parallel`, `sequential`, and other chunked strategies will do with your documents. Workflow example [#workflow-example] ```bash # Parse a PDF, inspect it in the browser before extracting struktur parse --input contract.pdf --images --screenshots --output contract-artifacts.json struktur utils artifact-viewer --input contract-artifacts.json --output viewer.html open viewer.html # Decide on chunking parameters, then extract struktur extract --input contract.pdf --images --schema schema.json \ --strategy parallelAutoMerge --chunk-size 8000 --model openai/gpt-4o ``` See also [#see-also] * [parse](/docs/cli/parse) — Generate artifact JSON from files * [Artifact Format](/docs/explanation/artifact-format) — Understanding artifacts * [Chunking & Token Budgets](/docs/explanation/chunking) — How chunking works # verify import { TypeTable } from 'fumadocs-ui/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; Synopsis [#synopsis] ```bash struktur verify --input struktur verify --stdin ``` Output on success: ```json { "valid": true, "artifacts": 2 } ``` Throws with a descriptive error on invalid artifact JSON (schema path, expected type, etc.). Usage [#usage] Use this to verify your preprocessing pipeline produces valid artifact format before running extraction. ```bash # From stdin cat artifacts.json | struktur verify --stdin # From a file struktur verify --input artifacts.json # Verify parse output struktur parse --input document.pdf | struktur verify --stdin ``` See also [#see-also] * [Artifact Format](/docs/explanation/artifact-format) — the JSON spec * [parse](/docs/cli/parse) — convert files to artifact JSON # Enrich Records from URLs The pattern [#the-pattern] You have records with URLs (e.g., customer records with contract URLs). You want to fetch each URL and extract additional data. CLI approach [#cli-approach] ```bash cat customers.json | jq -c '.[]' | while read -r row; do url=$(echo "$row" | jq -r '.contract_url') curl -s "$url" | struktur --stdin \ --schema-json '{"type":"object","properties":{"start_date":{"type":"string"},"value":{"type":"number"}},"required":["start_date","value"],"additionalProperties":false}' \ --model openai/gpt-4o-mini | \ jq --argjson orig "$row" '$orig + .' done | jq -s '.' ``` SDK [#sdk] ```js import { extract, simple } from "@struktur/sdk"; import { openai } from "@ai-sdk/openai"; import { parse } from "@struktur/sdk"; const schema = { type: "object", properties: { start_date: { type: "string" }, value: { type: "number" }, }, required: ["start_date", "value"], additionalProperties: false, }; async function enrichRecords(records) { const enriched = []; for (const record of records) { try { // Fetch URL content const response = await fetch(record.contract_url); const text = await response.text(); // Parse as artifact const artifacts = await parse({ kind: "text", text }); // Extract const result = await extract({ artifacts, schema, strategy: simple({ model: openai("gpt-4o-mini") }), }); // Merge back enriched.push({ ...record, ...result.data }); } catch (error) { console.error(`Failed to enrich: ${record.contract_url}`); enriched.push(record); // Keep original on failure } } return enriched; } const customers = [ { name: "Acme Corp", contract_url: "https://example.com/contracts/1" }, { name: "Globex", contract_url: "https://example.com/contracts/2" }, ]; const result = await enrichRecords(customers); console.log(result); ``` With concurrency [#with-concurrency] ```js import { extract, parallel } from "@struktur/sdk"; async function enrichRecordsParallel(records, concurrency = 5) { const results = await Promise.all( records.map(async (record, index) => { // Stagger requests to avoid rate limits await new Promise(r => setTimeout(r, index * 100)); const response = await fetch(record.contract_url); const text = await response.text(); const artifacts = await parse({ kind: "text", text }); const result = await extract({ artifacts, schema, strategy: simple({ model: openai("gpt-4o-mini") }), }); return { ...record, ...result.data }; }) ); return results; } ``` See also [#see-also] * [parse()](/docs/sdk/parse) — fetching pre-built artifacts * [Extraction Strategies](/docs/explanation/strategies) — strategy reference * [Shell Pipelines & Patterns](/docs/examples/pipelines) — more shell patterns # Extract Invoice Data Schema [#schema] CLI approach [#cli-approach] Single invoice: ```bash struktur --input invoice.pdf \ --schema invoice-schema.json \ --model openai/gpt-4o-mini ``` With embedded images (for invoices with stamps, logos, or handwritten amounts): ```bash struktur --input invoice.pdf --images \ --schema invoice-schema.json \ --model openai/gpt-4o ``` Multiple invoices: ```bash for file in invoices/*.pdf; do struktur --input "$file" \ --schema invoice-schema.json \ --model openai/gpt-4o-mini \ --output "outputs/$(basename $file .pdf).json" done ``` SDK [#sdk] Small invoices (1-3 pages): ```js import { extract, simple, parse } from "@struktur/sdk"; import { openai } from "@ai-sdk/openai"; const artifacts = await parse( { kind: "file", path: "invoice.pdf" }, { includeImages: true } ); const result = await extract({ artifacts, schema: invoiceSchema, strategy: simple({ model: openai("gpt-4o-mini") }), }); ``` Multi-page invoices with many line items: ```js import { extract, sequentialAutoMerge, parse } from "@struktur/sdk"; import { openai } from "@ai-sdk/openai"; const artifacts = await parse({ kind: "file", path: "invoice.pdf" }); const result = await extract({ artifacts, schema: invoiceSchema, strategy: sequentialAutoMerge({ model: openai("gpt-4o-mini"), dedupeModel: openai("gpt-4o-mini"), chunkSize: 8000, }), }); ``` Strategy choice [#strategy-choice] | Invoice type | Strategy | | ------------------------------------ | --------------------- | | 1-3 pages | `simple` | | Multi-page, line items may duplicate | `sequentialAutoMerge` | | Many invoices in parallel | `parallelAutoMerge` | Expected output [#expected-output] ```json { "invoice_number": "1042", "vendor": "Acme Corp", "invoice_date": "2024-03-01", "due_date": "2024-04-01", "currency": "USD", "line_items": [ { "description": "Widget A", "quantity": 10, "unit_price": 50, "total": 500 }, { "description": "Widget B", "quantity": 5, "unit_price": 200, "total": 1000 } ], "subtotal": 1500, "tax": 150, "total": 1650 } ``` See also [#see-also] * [Extraction Strategies](/docs/explanation/strategies) — strategy reference * [Process a Directory of Files](/docs/examples/process-directory) — batch processing # Extract Real Estate Listings Schema [#schema] ```json { "type": "object", "properties": { "property_name": { "type": "string" }, "address": { "type": "string" }, "description": { "type": "string" }, "total_units": { "type": "number" }, "units": { "type": "array", "items": { "type": "object", "properties": { "unit_number": { "type": "string" }, "size_sqm": { "type": "number" }, "rent_per_month": { "type": "number" }, "rooms": { "type": "number" }, "floor": { "type": "string" }, "features": { "type": "array", "items": { "type": "string" } } }, "required": ["unit_number", "size_sqm"], "additionalProperties": false } }, "images": { "type": "array", "items": { "type": "object", "properties": { "description": { "type": "string" }, "location": { "type": "string" } }, "additionalProperties": false } } }, "required": ["property_name", "address"], "additionalProperties": false } ``` Why sequentialAutoMerge [#why-sequentialautomerge] For real estate exposés: * **Context preservation:** Units on later pages can reference earlier context * **Deduplication:** Same unit may appear on multiple pages * **Image handling:** Processes embedded images alongside text CLI [#cli] ```bash struktur --input expose.pdf \ --schema property-schema.json \ --strategy sequentialAutoMerge \ --model openai/gpt-4o-mini ``` SDK [#sdk] ```js import { extract, sequentialAutoMerge } from "@struktur/sdk"; import { openai } from "@ai-sdk/openai"; import { fileToArtifact } from "@struktur/sdk"; import fs from "node:fs/promises"; const buffer = Buffer.from(await fs.readFile("expose.pdf")); const artifact = await fileToArtifact(buffer, { mimeType: "application/pdf" }); const result = await extract({ artifacts: [artifact], schema: propertySchema, strategy: sequentialAutoMerge({ model: openai("gpt-4o-mini"), dedupeModel: openai("gpt-4o-mini"), chunkSize: 8000, }), }); console.log(result.data); ``` Expected output [#expected-output] ```json { "property_name": "Hauptstraße 42", "address": "Hauptstraße 42, 10115 Berlin", "description": "Mixed-use commercial and residential building...", "total_units": 8, "units": [ { "unit_number": "1.1", "size_sqm": 85, "rent_per_month": 1200, "rooms": 3, "floor": "1st floor", "features": ["balcony", "parking space"] } ], "images": [ { "description": "Building exterior", "location": "front page" }, { "description": "Floor plan unit 1.1", "location": "page 3" } ] } ``` See also [#see-also] * [Extraction Strategies](/docs/explanation/strategies) — strategy reference and decision guide * [The Artifact Format](/docs/explanation/artifact-format) — handling images # Examples import { Card, Cards } from 'fumadocs-ui/components/card'; Practical examples showing how to use Struktur for common extraction tasks. # Shell Pipelines & Patterns Extract from a PDF (via markitdown) [#extract-from-a-pdf-via-markitdown] ```bash markitdown document.pdf | struktur --stdin --schema schema.json --model openai/gpt-4o-mini ``` Process a directory of files [#process-a-directory-of-files] ```bash find ./invoices -name "*.pdf" -print0 | while IFS= read -r -d '' f; do markitdown "$f" | struktur --stdin --schema invoice.json --model openai/gpt-4o-mini done | jq -s '.' ``` Pipe output to Postgres [#pipe-output-to-postgres] ```bash find ./invoices -name "*.pdf" -exec markitdown {} \; | \ struktur --stdin --schema invoice.json --model openai/gpt-4o-mini | \ jq '.line_items[]' | \ psql mydb -c "COPY line_items FROM STDIN (FORMAT csv)" ``` Watch a folder for new files (Linux) [#watch-a-folder-for-new-files-linux] ```bash inotifywait -m ./incoming -e create -e moved_to | while read -r path action file; do [[ "$file" == *.pdf ]] && markitdown "$path/$file" | \ struktur --stdin --schema invoice.json --model openai/gpt-4o-mini \ >> processed.jsonl done ``` Watch a folder for new files (macOS) [#watch-a-folder-for-new-files-macos] ```bash fswatch -o ./incoming | while read f; do for file in ./incoming/*; do [ -f "$file" ] || continue markitdown "$file" | struktur --stdin \ --schema invoice.json \ --model openai/gpt-4o-mini \ --output "processed/$(basename $file).json" mv "$file" ./processed/ done done ``` Enrich records from URLs [#enrich-records-from-urls] ```bash cat contracts.json | jq -c '.[]' | while read -r row; do url=$(echo "$row" | jq -r '.contract_url') curl -s "$url" | struktur --stdin \ --schema-json '{"type":"object","properties":{"start_date":{"type":"string"},"value":{"type":"number"}},"required":["start_date","value"],"additionalProperties":false}' \ --model openai/gpt-4o-mini | \ jq --argjson orig "$row" '$orig + .' done | jq -s '.' ``` Test a schema against samples [#test-a-schema-against-samples] ```bash for f in samples/*.pdf; do echo "Testing: $f" markitdown "$f" | struktur --stdin --schema v2.json --model openai/gpt-4o-mini 2>&1 | \ jq -e '.' && echo "OK: $f" || echo "FAILED: $f" done ``` Save to file instead of stdout [#save-to-file-instead-of-stdout] ```bash struktur --input report.pdf --schema schema.json --model openai/gpt-4o-mini --output result.json ``` See also [#see-also] * [extract](/docs/cli/extract) — CLI flags * [Process a Directory of Files](/docs/examples/process-directory) — detailed example * [Watch a Folder for New Files](/docs/examples/watch-folder) — detailed example # Process a Directory of Files Shell loop with find [#shell-loop-with-find] ```bash find ./documents -name "*.pdf" -print0 | while IFS= read -r -d '' file; do echo "Processing: $file" struktur --input "$file" \ --schema schema.json \ --model openai/gpt-4o-mini \ --output "outputs/$(basename "$file" .pdf).json" done ``` With markitdown for PDFs [#with-markitdown-for-pdfs] ```bash for file in documents/*.pdf; do markitdown "$file" | struktur --stdin \ --schema schema.json \ --model openai/gpt-4o-mini \ --output "outputs/$(basename "$file" .pdf).json" done ``` Error handling script [#error-handling-script] ```bash #!/bin/bash SCHEMA="schema.json" INPUT_DIR="./documents" OUTPUT_DIR="./outputs" MODEL="openai/gpt-4o-mini" mkdir -p "$OUTPUT_DIR" for file in "$INPUT_DIR"/*.{pdf,txt,docx}; do [ -e "$file" ] || continue filename=$(basename "$file") output_file="$OUTPUT_DIR/${filename%.*}.json" echo "[$((++count))] Processing: $filename" if struktur --input "$file" \ --schema "$SCHEMA" \ --model "$MODEL" \ --output "$output_file"; then echo " ✓ Success: $output_file" else echo " ✗ Failed: $filename" >&2 fi done echo "Processed $count files" ``` SDK with parallel processing [#sdk-with-parallel-processing] ```js import { extract, parallelAutoMerge } from "@struktur/sdk"; import { openai } from "@ai-sdk/openai"; import { fileToArtifact } from "@struktur/sdk"; import fs from "node:fs/promises"; import path from "node:path"; const schema = /* your schema */; async function processDirectory(inputDir, outputDir) { await fs.mkdir(outputDir, { recursive: true }); const files = await fs.readdir(inputDir); const documents = files.filter(f => f.endsWith('.pdf') || f.endsWith('.txt') ); for (const [index, filename] of documents.entries()) { console.log(`[${index + 1}/${documents.length}] ${filename}`); try { const buffer = await fs.readFile(path.join(inputDir, filename)); const artifact = await fileToArtifact(buffer, { mimeType: filename.endsWith('.pdf') ? 'application/pdf' : 'text/plain' }); const result = await extract({ artifacts: [artifact], schema, strategy: parallelAutoMerge({ model: openai("gpt-4o-mini"), dedupeModel: openai("gpt-4o-mini") }) }); const outputPath = path.join( outputDir, `${path.parse(filename).name}.json` ); await fs.writeFile(outputPath, JSON.stringify(result.data, null, 2)); console.log(` ✓ Saved to ${outputPath}`); } catch (error) { console.error(` ✗ Failed: ${error.message}`); } } } await processDirectory("./documents", "./outputs"); ``` Aggregate output [#aggregate-output] Collect all results into a single array: ```bash for f in documents/*.pdf; do struktur --input "$f" --schema schema.json --model openai/gpt-4o-mini done | jq -s '.' ``` See also [#see-also] * [Watch a Folder for New Files](/docs/examples/watch-folder) — continuous processing * [Extraction Strategies](/docs/explanation/strategies) — strategy reference * [Shell Pipelines & Patterns](/docs/examples/pipelines) — more shell patterns # Watch a Folder for New Files Linux: inotifywait [#linux-inotifywait] For PDFs and other supported formats, use `--input` directly — no pre-processing required: ```bash inotifywait -m ./incoming -e create -e moved_to | while read -r path action file; do echo "New file: $file" struktur --input "$path/$file" \ --schema schema.json \ --model openai/gpt-4o-mini \ --output "processed/$file.json" mv "$path/$file" ./processed/ done ``` For formats without a built-in parser, pipe through a conversion tool first: ```bash inotifywait -m ./incoming -e create -e moved_to | while read -r path action file; do echo "New file: $file" markitdown "$path/$file" | struktur --stdin \ --schema schema.json \ --model openai/gpt-4o-mini \ --output "processed/$file.json" mv "$path/$file" ./processed/ done ``` macOS: fswatch [#macos-fswatch] ```bash fswatch -o ./incoming | while read f; do for file in ./incoming/*; do [ -f "$file" ] || continue echo "Processing: $file" struktur --input "$file" \ --schema schema.json \ --model openai/gpt-4o-mini \ --output "processed/$(basename $file).json" mv "$file" ./processed/ done done ``` Output to JSONL [#output-to-jsonl] For streaming ingestion, append to a JSONL file (one JSON object per line): ```bash inotifywait -m ./incoming -e create -e moved_to | while read -r path action file; do struktur --input "$path/$file" \ --schema schema.json \ --model openai/gpt-4o-mini \ >> processed.jsonl mv "$path/$file" ./processed/ done ``` SDK: fs.watch [#sdk-fswatch] ```js import { watch } from "node:fs"; import { extract, simple, parse } from "@struktur/sdk"; import { openai } from "@ai-sdk/openai"; import fs from "node:fs/promises"; import path from "node:path"; const schema = /* your schema */; const incomingDir = "./incoming"; const processedDir = "./processed"; await fs.mkdir(processedDir, { recursive: true }); const watcher = watch(incomingDir, async (event, filename) => { if (!filename || event !== "rename") return; const filePath = path.join(incomingDir, filename); try { await fs.access(filePath); } catch { return; // File was deleted, not created } console.log(`Processing: ${filename}`); try { // parse handles MIME detection and parsing (PDF, text, images, etc.) const artifacts = await parse({ kind: "file", path: filePath }); const result = await extract({ artifacts, schema, strategy: simple({ model: openai("gpt-4o-mini") }), }); const outputPath = path.join(processedDir, `${filename}.json`); await fs.writeFile(outputPath, JSON.stringify(result.data, null, 2)); await fs.unlink(filePath); console.log(` ✓ Processed: ${filename}`); } catch (error) { console.error(` ✗ Failed: ${error.message}`); } }); console.log(`Watching ${incomingDir}...`); ``` SDK: chokidar [#sdk-chokidar] For more robust file watching: ```js import chokidar from "chokidar"; import { extract, simple, parse } from "@struktur/sdk"; import { openai } from "@ai-sdk/openai"; import fs from "node:fs/promises"; import path from "node:path"; const schema = /* your schema */; const watcher = chokidar.watch("./incoming", { ignored: /(^|[\/\\])\../, persistent: true, awaitWriteFinish: { stabilityThreshold: 2000, pollInterval: 100 }, }); watcher.on("add", async (filePath) => { console.log(`Processing: ${path.basename(filePath)}`); try { const artifacts = await parse({ kind: "file", path: filePath }); const result = await extract({ artifacts, schema, strategy: simple({ model: openai("gpt-4o-mini") }), }); const outputPath = `./processed/${path.basename(filePath)}.json`; await fs.writeFile(outputPath, JSON.stringify(result.data, null, 2)); await fs.unlink(filePath); console.log(` ✓ Processed`); } catch (error) { console.error(` ✗ Failed: ${error.message}`); } }); console.log("Watching ./incoming..."); ``` See also [#see-also] * [Process a Directory of Files](/docs/examples/process-directory) — batch processing * [Shell Pipelines & Patterns](/docs/examples/pipelines) — more shell patterns * [Extraction Strategies](/docs/explanation/strategies) — strategy reference # Artifact Format The normalization boundary [#the-normalization-boundary] Different document types (PDF, HTML, Excel, email) require different parsing strategies. But LLM extraction is the same regardless of source format. The Artifact is the normalized form that crosses that boundary. Struktur only cares about what is in the artifact, not where it came from. What an artifact contains [#what-an-artifact-contains] An artifact has: * `id`: unique identifier * `type`: type hint (`text`, `image`, `pdf`, `file`) * `contents`: a sequence of content slices Each content slice may have: * `text`: the text content * `page`: page number (for paginated documents) * `media`: embedded images This structure naturally maps to paginated documents (each page is a content slice) or segmented text (each paragraph/section is a slice). Why text + images together? [#why-text--images-together] Some documents (real estate exposés, product datasheets) have critical information in images. Because images are embedded directly in content slices alongside text, the LLM sees them in context. Image limits per chunk are configurable on parallel strategies via `maxImages`. Complete specification [#complete-specification] JSON Schema [#json-schema] ```json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "SerializedArtifacts", "oneOf": [ { "$ref": "#/definitions/SerializedArtifact" }, { "type": "array", "items": { "$ref": "#/definitions/SerializedArtifact" }, "minItems": 1 } ], "definitions": { "SerializedArtifact": { "type": "object", "required": ["id", "type", "contents"], "additionalProperties": false, "properties": { "id": { "type": "string" }, "type": { "type": "string", "enum": ["text", "image", "pdf", "file"] }, "contents": { "type": "array", "items": { "$ref": "#/definitions/SerializedArtifactContent" }, "minItems": 1 }, "metadata": { "type": "object" }, "tokens": { "type": "number" } } }, "SerializedArtifactContent": { "type": "object", "additionalProperties": false, "properties": { "page": { "type": "number" }, "text": { "type": "string" }, "media": { "type": "array", "items": { "$ref": "#/definitions/SerializedArtifactImage" } } }, "anyOf": [ { "required": ["text"] }, { "required": ["media"] } ] }, "SerializedArtifactImage": { "type": "object", "required": ["type"], "additionalProperties": false, "properties": { "type": { "type": "string", "const": "image" }, "url": { "type": "string" }, "base64": { "type": "string" }, "text": { "type": "string" }, "x": { "type": "number" }, "y": { "type": "number" }, "width": { "type": "number" }, "height": { "type": "number" }, "imageType": { "type": "string", "enum": ["embedded", "screenshot"] } }, "anyOf": [ { "required": ["url"] }, { "required": ["base64"] } ] } } } ``` Top-level shape [#top-level-shape] | Field | Required | Description | | ---------- | -------- | -------------------------------------- | | `id` | Yes | Unique identifier | | `type` | Yes | One of: `text`, `image`, `pdf`, `file` | | `contents` | Yes | Array of content slices (at least one) | | `metadata` | No | Pass-through metadata object | | `tokens` | No | Pre-computed token count hint | Accepted as: a single object or an array `[{...}, {...}]`. Content slices [#content-slices] Each item in `contents` has: | Field | Required | Description | | ------- | -------- | -------------------------------------- | | `page` | No | Page number for paginated documents | | `text` | No | Text content of this slice | | `media` | No | Array of images embedded in this slice | At least one of `text` or `media` must be present. Images [#images] Each item in `media` has: | Field | Required | Description | | --------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `type` | Yes | Must be `"image"` | | `url` | No | URL to image (mutually exclusive with `base64`) | | `base64` | No | Base64-encoded image data (no data-URL prefix) | | `text` | No | Alt text or OCR output | | `x`, `y`, `width`, `height` | No | Optional spatial metadata (pixels) | | `imageType` | No | `"embedded"` or `"screenshot"`. Distinguishes images extracted from the document body from page renders. Omit for hand-crafted artifacts. | Either `url` or `base64` must be present. The `imageType` field is set automatically by the PDF parser: `"embedded"` for images extracted from the PDF body (requires `--images`), `"screenshot"` for full-page renders (requires `--screenshots`). The artifact viewer uses this field to filter and badge images independently. Complete example [#complete-example] ```json [ { "id": "invoice-2024-1042", "type": "pdf", "contents": [ { "page": 1, "text": "INVOICE\nInvoice #: 1042\nDate: 2024-03-01\nBill To: Acme Corp\n...", "media": [ { "type": "image", "base64": "iVBORw0KGgoAAAANS...", "text": "Company logo", "imageType": "embedded" }, { "type": "image", "base64": "iVBORw0KGgoAAAANS...", "imageType": "screenshot" } ] }, { "page": 2, "text": "Line Items:\n- Widget A x10 @ $50.00 = $500.00\n- Widget B x5 @ $200.00 = $1,000.00\nTotal: $1,500.00" } ], "metadata": { "filename": "invoice-1042.pdf", "source": "email-attachment" } } ] ``` Validation [#validation] Struktur validates artifact JSON before processing. Use the CLI: ```bash # From stdin cat artifacts.json | struktur verify --stdin # or from a file: struktur verify --input artifacts.json ``` Returns `{ "valid": true, "artifacts": 1 }` on success, throws with error detail on failure. Built-in artifact creation [#built-in-artifact-creation] | Path | Description | | ----------------------- | --------------------------------------------------------------------------------- | | `--input ` (CLI) | MIME detection + parser resolution; PDF uses built-in `parsePdf` | | `--stdin` (CLI) | MIME detection on buffer; `text/plain` falls back to text artifact | | `parse()` (SDK) | Accepts `kind: "text"`, `kind: "file"`, `kind: "buffer"`, `kind: "artifact-json"` | | `urlToArtifact()` (SDK) | Fetches URL, validates as `SerializedArtifact[]` | See also [#see-also] * [Document Parsing](/docs/explanation/document-parsing) — how to get input into Struktur and how files are converted to artifacts * [parse()](/docs/sdk/parse) — the SDK API # Chunking & Token Budgets The context window problem [#the-context-window-problem] LLMs have finite context windows. Documents — especially multi-page PDFs, large datasets, or many files at once — often exceed them. Struktur's chunker splits artifact contents into batches that fit within a configurable token budget (`chunkSize`). How splitting works [#how-splitting-works] Splitting happens at two levels: 1. **ArtifactSplitter:** splits a single large artifact's contents into smaller parts, respecting content slice boundaries (e.g., page boundaries). 2. **ArtifactBatcher:** groups artifacts or artifact parts into batches that stay within the token budget and optional image count limit. The tokenizer uses a character-approximation (not exact token counting). `chunkSize` defaults to 10,000 tokens. Images and maxImages [#images-and-maximages] Some strategies accept `maxImages` to cap how many images appear per chunk. This matters when images consume disproportionate context. If a batch would exceed `maxImages`, extra images are moved to the next batch. Why simple does not chunk [#why-simple-does-not-chunk] The `simple` strategy loads all artifacts as-is. If the input exceeds the context window, the LLM call may fail or produce degraded results. For large inputs, use a chunked strategy. This is a deliberate trade-off: `simple` is fast and cheap for small inputs, not suitable for large ones. Merging partial results [#merging-partial-results] When a document is split into N chunks and each chunk is extracted independently, you get N partial objects. For a scalar-heavy schema (title, author, date), you want the best answer from all chunks. For an array-heavy schema (line items, product listings), you want to concatenate all arrays and remove duplicates. LLM merge (parallel, doublePass) [#llm-merge-parallel-doublepass] These strategies send all partial results to a **merge model** in a single call, with a prompt asking it to produce a single coherent output. The merge model sees the full schema and all partial outputs. This is powerful but costs extra tokens. Schema-aware auto-merge [#schema-aware-auto-merge] `parallelAutoMerge`, `sequentialAutoMerge`, and `doublePassAutoMerge` use `SmartDataMerger`: * **Arrays:** concatenated. `items` from chunk 1 + `items` from chunk 2 = `items` in merged. * **Objects:** shallow-merged. Keys from later chunks overwrite keys from earlier ones. * **Scalars:** prefer newer non-empty values. This approach avoids an extra LLM call and works well for list-extraction schemas. It does not handle complex cross-chunk synthesis — for that, use LLM merge. Deduplication [#deduplication] After auto-merging concatenated arrays, there may be duplicates. Dedup runs in two stages: **Stage 1: CRC32 hash-based.** Exact duplicates (byte-for-byte identical after stable JSON stringification) are removed without any LLM call. Fast and cheap. **Stage 2: LLM-based semantic dedup.** A dedupe model is given the merged array and asked to identify semantically equivalent entries (e.g., "iPhone 15" vs "Apple iPhone 15 128GB"). It returns a list of dot-path keys to remove (e.g., `items.3`). Only the auto-merge variants include this step. When dedup matters [#when-dedup-matters] Dedup is valuable when: * The same item legitimately appears in multiple chunks. * The same document segment appears in multiple artifacts. Dedup adds token cost and latency. For schemas without arrays, or for inputs with no expected overlap, use strategies without auto-merge. See also [#see-also] * [The Extraction Pipeline](/docs/explanation/pipeline) — the full flow * [Extraction Strategies](/docs/explanation/strategies) — which strategies use chunking # Document Parsing Struktur's parser system converts files into [Artifact format](/docs/explanation/artifact-format) before any LLM work happens. Parsers are resolved by MIME type and are fully configurable. MIME Detection [#mime-detection] MIME type is detected in three layers (tried in order): 1. **Magic bytes** (authoritative): PDF (`%PDF-`), PNG, JPEG, GIF, WebP, and ZIP-based Office formats are identified from the first bytes of the file. 2. **npm `detectFileType` callback**: A custom npm parser may export a `detectFileType(header: Uint8Array): boolean` function to claim MIME types beyond what magic bytes cover. 3. **File extension database**: Fallback for inputs where magic bytes don't match a known signature. Override MIME detection with `--mime ` on any command that accepts input. Built-in Parsers [#built-in-parsers] | MIME type | Behavior | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `application/pdf` | Per-page text via `pdf-parse`. Embedded images require `--images`. Page screenshots require `--screenshots`. Image deduplication filters images smaller than \~80px. Non-fatal on image/screenshot failures. | | `text/*` | Split on double newlines into content slices. | | `image/*` | Single-content artifact with one media item. | | `application/json` | If it validates as `SerializedArtifact[]`, passed through unchanged without invoking any parser. | Built-in Input Types [#built-in-input-types] Plain text / markdown (CLI) [#plain-text--markdown-cli] | Flag | Description | | ----------------- | --------------------------------------------------------------------------------------------------------- | | `--stdin` | Reads stdin as UTF-8 text. Auto-detected when piped with no other input flag. | | `--text ` | Inline text as a CLI argument. | | `--input ` | Reads a file. MIME type auto-detected; text files become text artifacts, PDFs invoke the PDF parser, etc. | Text is split on double newlines into content slices automatically. Artifact JSON (CLI) [#artifact-json-cli] | Flag | Description | | ----------------------------- | ------------------------------------------------------------ | | `--stdin` | Reads stdin. Auto-detects artifact JSON or raw text. | | `--artifact-file ` | Reads pre-built artifact JSON from file path or HTTP(S) URL. | | `--artifact-json ` | Inline artifact JSON string. | Both accept a single artifact object or an array. Schema loading [#schema-loading] | Flag | Description | | ---------------------- | ------------------------------------------------ | | `--schema ` | JSON Schema file (local path or HTTP/HTTPS URL). | | `--schema-json ` | Inline JSON Schema string. | Schema loading from URLs sends `Accept: application/schema+json, application/json` headers. Custom Parsers [#custom-parsers] There are three ways to extend Struktur with support for new file formats: 1. **Configuration-level (recommended):** `struktur config parsers add` — zero code, works in CLI and SDK via `parserConfig` 2. **SDK-level inline parser:** Add an `InlineParserDef` to `parserConfig` — code-only, works with `parse()` 3. **Legacy providers:** Use the deprecated `providers` registry with `fileToArtifact()` Option 1: Configuration-level (recommended) [#option-1-configuration-level-recommended] Register a parser by MIME type using the CLI. This works transparently for all `--input` and `parse` calls. ```bash # npm package parser struktur config parsers add \ --mime application/vnd.ms-excel \ --npm @myorg/xlsx-parser # Shell command with file path struktur config parsers add \ --mime application/vnd.openxmlformats-officedocument.wordprocessingml.document \ --file-command "markitdown FILE_PATH" ``` See [config parsers](/docs/cli/config#config-parsers) for the full reference. For the SDK, pass a `parserConfig` to `parse`: ```typescript import { parse } from "@struktur/sdk"; const artifacts = await parse( { kind: "file", path: "report.xlsx" }, { parserConfig: { "application/vnd.ms-excel": { type: "npm", package: "@myorg/xlsx-parser" }, }, } ); ``` Option 2: SDK-level inline parser [#option-2-sdk-level-inline-parser] Add an `InlineParserDef` to your `parserConfig`. This is the modern way to register code-only parsers that work with `parse()`. ```typescript import { parse } from "@struktur/sdk"; import * as XLSX from "xlsx"; const artifacts = await parse( { kind: "file", path: "report.xlsx" }, { parserConfig: { "application/vnd.ms-excel": { type: "inline", handler: async (buffer) => { const workbook = XLSX.read(buffer); const contents = workbook.SheetNames.map((name, i) => ({ page: i + 1, text: XLSX.utils.sheet_to_csv(workbook.Sheets[name]), })); return { id: `excel-${crypto.randomUUID()}`, type: "file", raw: async () => buffer, contents, }; }, }, }, } ); ``` The inline parser signature [#the-inline-parser-signature] An inline parser is an async function that takes a Buffer and returns an Artifact: ```typescript type InlineParserHandler = (buffer: Buffer) => Promise; const myParser: InlineParserHandler = async (buffer) => { const pages = await parseMyFormat(buffer); return { id: `doc-${crypto.randomUUID()}`, type: "file", raw: async () => buffer, contents: pages.map((page, i) => ({ page: i + 1, text: page.text, media: page.images.map((img) => ({ type: "image", base64: img.base64, })), })), }; }; ``` Common patterns [#common-patterns] Excel with xlsx package [#excel-with-xlsx-package] ```typescript import * as XLSX from "xlsx"; import type { InlineParserDef } from "@struktur/sdk"; const excelParser: InlineParserDef = { type: "inline", handler: async (buffer) => { const workbook = XLSX.read(buffer); const contents = workbook.SheetNames.map((name, i) => ({ page: i + 1, text: XLSX.utils.sheet_to_csv(workbook.Sheets[name]), })); return { id: `excel-${crypto.randomUUID()}`, type: "file", raw: async () => buffer, contents, }; }, }; ``` Email with mailparser [#email-with-mailparser] ```typescript import { simpleParser } from "mailparser"; import type { InlineParserDef } from "@struktur/sdk"; const emailParser: InlineParserDef = { type: "inline", handler: async (buffer) => { const parsed = await simpleParser(buffer); return { id: `email-${crypto.randomUUID()}`, type: "text", raw: async () => buffer, contents: [{ text: `Subject: ${parsed.subject}\n\n${parsed.text}`, }], metadata: { from: parsed.from?.text, date: parsed.date, }, }; }, }; ``` Option 3: Legacy providers (deprecated) [#option-3-legacy-providers-deprecated] The old `providers` registry is deprecated. Use `InlineParserDef` in `parserConfig` instead. If you need backward compatibility, you can still use `fileToArtifact` with the providers registry: ```js import { fileToArtifact } from "@struktur/sdk"; import { readFile } from "node:fs/promises"; const buffer = Buffer.from(await readFile("document.xlsx")); const artifact = await fileToArtifact(buffer, { mimeType: "application/vnd.ms-excel", providers: { "application/vnd.ms-excel": myProvider, }, }); ``` **Note:** This approach does not support MIME detection or the parser system — it only applies when you call `fileToArtifact` directly with an explicit `mimeType`. npm Package Parser [#npm-package-parser] Install a package that implements the `NpmParserModule` interface: ```typescript import type { Artifact } from "@struktur/sdk"; // At least one of these is required: export async function parseStream( stream: ReadableStream, mimeType: string ): Promise; export async function parseFile( filePath: string, mimeType: string ): Promise; // Optional: return true if your parser handles these bytes export function detectFileType(header: Uint8Array): boolean; ``` When both `parseFile` and `parseStream` are exported, Struktur prefers `parseFile` for file inputs (zero-copy) and `parseStream` for buffer or stdin inputs. A temp file is created as a fallback if needed. Register it: ```bash struktur config parsers add \ --mime application/vnd.openxmlformats-officedocument.wordprocessingml.document \ --npm @myorg/docx-parser ``` Shell Command Parsers [#shell-command-parsers] File-based [#file-based] The `FILE_PATH` placeholder is replaced with the actual file path at runtime. For buffer inputs, a temp file is created automatically. ```bash struktur config parsers add \ --mime application/vnd.ms-excel \ --file-command "python3 /path/to/excel2artifact.py FILE_PATH" ``` `FILE_PATH` must appear in the command string — an error is thrown if it is missing. The command must write `SerializedArtifact[]` JSON to stdout. Stdin-based [#stdin-based] File contents are piped to the command's stdin. ```bash struktur config parsers add \ --mime text/html \ --stdin-command "my-html-to-artifact-tool" ``` The command must write `SerializedArtifact[]` JSON to stdout. Plain text output will fail validation. Parser Resolution Order [#parser-resolution-order] For any input, parsers are resolved in this order: 1. `--parser ` flag on the CLI — always wins, bypasses all config 2. Parser configured for the detected MIME type (`config parsers add`) 3. Built-in parser (PDF, text/\*, image/\*, JSON) 4. Error with a suggestion to use `config parsers add` Ad-hoc Parser Override [#ad-hoc-parser-override] Use `--parser` to override the configured parser for a single run without changing config: ```bash struktur parse --input report.docx --parser @myorg/experimental-docx-parser struktur --input data.xlsx --parser @myorg/xlsx-parser --fields "..." --model openai/gpt-4o-mini ``` SDK Usage [#sdk-usage] parse(input, options?) [#parseinput-options] The primary SDK function for loading input into artifacts. Handles MIME detection and parser resolution automatically. ```typescript import { parse } from "@struktur/sdk"; const artifacts = await parse( { kind: "file", path: "document.pdf" }, { parserConfig: parsersConfig, // ParsersConfig — keyed by MIME type (optional) includeImages: true, // extract embedded PDF images screenshots: false, // render PDF page screenshots screenshotScale: 1.5, // scale factor for screenshots screenshotWidth: undefined, // target width in pixels (overrides screenshotScale) } ); ``` Supported input kinds: | Input kind | Description | | -------------------------------------- | ------------------------------------------------------- | | `{ kind: "text", text }` | Text artifact (split on double newlines) | | `{ kind: "file", path, mimeType? }` | File artifact with MIME detection and parser resolution | | `{ kind: "buffer", buffer, mimeType }` | Buffer artifact with parser resolution | | `{ kind: "artifact-json", data }` | Validates and hydrates pre-built artifact JSON | When `kind: "file"` is used, MIME detection and parser resolution happen automatically based on `parserConfig`. fileToArtifact(buffer, options) [#filetoartifactbuffer-options] Lower-level helper that creates an artifact from a Buffer. **Deprecated:** use `parse()` with `InlineParserDef` in `parserConfig` instead. **Important:** `fileToArtifact` uses the legacy `providers` registry, which does not include the built-in PDF parser or any parsers configured via `config parsers add`. For PDF and other format support, use `parse` instead. ```js import { fileToArtifact } from "@struktur/sdk"; import fs from "node:fs/promises"; const buffer = Buffer.from(await fs.readFile("document.txt")); const artifact = await fileToArtifact(buffer, { mimeType: "text/plain", providers: { /* deprecated — use parserConfig with InlineParserDef instead */ } }); ``` urlToArtifact(url) [#urltoartifacturl] Fetches a URL and expects it to return pre-serialized artifact JSON. Validates and hydrates. ```js import { urlToArtifact } from "@struktur/sdk"; const artifacts = await urlToArtifact("https://example.com/artifact.json"); ``` See also [#see-also] * [config parsers](/docs/cli/config#config-parsers) — CLI commands for managing parsers * [parse](/docs/cli/parse) — Convert files to artifact JSON * [Artifact Format](/docs/explanation/artifact-format) — The output data structure * [parse()](/docs/sdk/parse) — SDK API # Explanation import { Card, Cards } from 'fumadocs-ui/components/card'; Deep dives into Struktur's architecture, strategies, and design decisions. # Models and Providers import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; Struktur is built on the [Vercel AI SDK](https://sdk.vercel.ai/docs/introduction), which provides a unified interface to multiple LLM providers. This architecture makes it straightforward to use any model from supported providers—or add new ones. Supported Providers [#supported-providers] Struktur currently supports the following providers out of the box: | Provider | Environment Variable | Package | | -------------- | ------------------------------ | ----------------------------- | | **OpenAI** | `OPENAI_API_KEY` | `@ai-sdk/openai` | | **Anthropic** | `ANTHROPIC_API_KEY` | `@ai-sdk/anthropic` | | **Google** | `GOOGLE_GENERATIVE_AI_API_KEY` | `@ai-sdk/google` | | **OpenCode** | `OPENCODE_API_KEY` | `@ai-sdk/openai`\* | | **OpenRouter** | `OPENROUTER_API_KEY` | `@openrouter/ai-sdk-provider` | \*OpenCode uses the OpenAI-compatible API via the Vercel SDK's OpenAI provider. **Model names change frequently.** Rather than document specific models, Struktur focuses on provider integration. Check your provider's documentation for available models and their capabilities. Specifying Models [#specifying-models] Models are specified using the format `provider/model-name`: ```typescript import { extract } from "@struktur/sdk"; // OpenAI const result = await extract({ artifacts, schema, strategy: { type: "simple", model: "openai/gpt-4o" } }); // Anthropic const result = await extract({ artifacts, schema, strategy: { type: "simple", model: "anthropic/claude-3-5-sonnet" } }); // Google const result = await extract({ artifacts, schema, strategy: { type: "simple", model: "google/gemini-1.5-pro" } }); ``` Authentication [#authentication] Struktur supports two authentication methods: Environment Variables [#environment-variables] Set the appropriate API key for your provider: ```bash export OPENAI_API_KEY="sk-..." export ANTHROPIC_API_KEY="sk-ant-..." export GOOGLE_GENERATIVE_AI_API_KEY="..." export OPENCODE_API_KEY="..." export OPENROUTER_API_KEY="..." ``` Secure Token Storage [#secure-token-storage] For CLI usage, Struktur can store tokens securely: ```bash # Store in macOS Keychain (preferred on macOS) struktur auth set --provider openai --token "sk-..." # Or store in file struktur auth set --provider openai --token "sk-..." --storage file ``` On macOS, Struktur defaults to the system Keychain. On other platforms, tokens are stored in `~/.config/struktur/tokens.json` with strict permissions (`0o600`). Special Providers [#special-providers] OpenCode (PyCoding Agent) [#opencode-pycoding-agent] OpenCode provides access to multiple model families through a single API: ```typescript // OpenAI-compatible models "opencode/gpt-5.2" "opencode/gpt-5.1" // Anthropic-compatible models "opencode/claude-opus-4-6" "opencode/claude-sonnet-4-5" // Google-compatible models "opencode/gemini-3.1-pro" "opencode/gemini-3-flash" // Other providers "opencode/kimi-k2.5" "opencode/glm-5" ``` Struktur automatically routes OpenCode requests to the correct Vercel SDK provider based on the model prefix (`gpt-`, `claude-`, `gemini-`). OpenRouter [#openrouter] OpenRouter provides access to models from multiple providers through a unified API. You can also specify a preferred upstream provider: ```typescript // Basic usage "openrouter/anthropic/claude-3.5-sonnet" // With preferred provider (using hashtag syntax) "openrouter/anthropic/claude-3.5-sonnet#octoai" ``` Adding New Providers [#adding-new-providers] Because Struktur uses the Vercel AI SDK, adding support for new providers is straightforward: 1. **Check if Vercel AI SDK supports the provider** The Vercel AI SDK has a growing ecosystem of [community providers](https://sdk.vercel.ai/providers/community-providers). If your provider is listed there, integration is simple. 2. **Create a provider resolver** Add a case to `resolveModel` in `packages/sdk/src/llm/resolveModel.ts`: ```typescript case "newprovider": { const { createNewProvider } = await import("@ai-sdk/newprovider"); return createNewProvider({ apiKey })(modelName); } ``` 3. **Add environment variable mapping** Update `resolveProviderEnvVar` in `packages/sdk/src/auth/tokens.ts`: ```typescript case "newprovider": return "NEWPROVIDER_API_KEY"; ``` 4. **(Optional) Add model listing support** If the provider has a models API, add support in `packages/sdk/src/llm/models.ts`. Model Capabilities [#model-capabilities] When selecting a model, consider: | Capability | Considerations | | --------------------- | ---------------------------------------------------------------- | | **Structured Output** | All supported providers support JSON schema output | | **Vision/Multimodal** | Check if the model supports image input for PDF/image extraction | | **Context Window** | Larger documents require models with larger context windows | | **Rate Limits** | Consider provider rate limits for batch processing | | **Cost** | Different models have vastly different pricing | Not all models support image inputs. If you're extracting from PDFs or images with visual content, use a vision-capable model (e.g., GPT-4o, Claude 3.5 Sonnet, Gemini 1.5 Pro). Listing Available Models [#listing-available-models] The CLI can list available models from configured providers: ```bash # List models for a specific provider struktur models --provider openai # List models for all configured providers struktur models # Pick the cheapest available model struktur extract --model cheapest --provider openai ``` See Also [#see-also] * [Extraction Strategies](/docs/explanation/strategies) — How strategies use models * [CLI Authentication](/docs/cli/config) — Managing provider tokens * [Vercel AI SDK Documentation](https://sdk.vercel.ai/docs) — Full provider documentation # Extraction Lifecycle import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { TypeTable } from 'fumadocs-ui/components/type-table'; ```mermaid flowchart LR A[Input] --> B[Parse] B --> C[Artifacts] C --> D[Strategy] D --> E[Output] subgraph StrategyInternals [Strategy] direction TB D1[Chunking] --> D2[LLM Calls] D2 --> D3[Validation + Retry] D3 --> D4[Merge/Dedupe] end D --> StrategyInternals --> E ``` Inputs and Artifacts [#inputs-and-artifacts] Struktur converts input files into **Artifacts** before extraction. For plain text or stdin, this is trivial. For structured files (PDFs, Office documents), Struktur runs a parser — built-in or custom — that extracts text and images per-page. The Strategy layer [#the-strategy-layer] A strategy is the orchestration engine. It decides how to split the input, how many LLM calls to make, whether to run them concurrently or sequentially, and how to combine results. Built-in strategies cover the common patterns. You can also write your own. See [Strategies](/docs/explanation/strategies) for the complete strategy reference. Validation inside the loop [#validation-inside-the-loop] The validation loop is a key differentiator. Every LLM response is validated against the schema **before** the strategy considers it done. If validation fails, the errors are serialized and sent back to the model as a follow-up message. **Smart validation**: For multi-step strategies (parallel, sequential, double-pass), Struktur uses lenient validation during intermediate steps—required field violations are allowed until the final step. This prevents false failures when data is split across chunks. Use the `strict` option to disable this behavior. Most extractions converge within two attempts. This happens **inside** the strategy, not as a post-processing step. Default: `maxAttempts` = 3. See [Validation & Retries](/docs/explanation/validation) for the validation concept. The result [#the-result] See also [#see-also] * [Document Parsing](/docs/explanation/document-parsing) — how files are converted to artifacts * [Artifacts](/docs/explanation/artifact-format) — the input format * [Strategies](/docs/explanation/strategies) — orchestration patterns * [Chunking & Token Budgets](/docs/explanation/chunking) — how large documents are split * [Validation & Retries](/docs/explanation/validation) — the retry loop # Extraction Strategies import { TypeTable } from 'fumadocs-ui/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; The **Agent** strategy is the default and recommended way to use Struktur. It gives the LLM a virtual filesystem and lets it autonomously decide how to extract your data. For documents where you need more control, Struktur also provides alternative strategies that use fixed chunking and parallelism patterns. Strategy comparison [#strategy-comparison] | Strategy | Speed | Context | Arrays | Token Cost | Best For | | --------------------- | -------- | -------- | ------------- | ---------- | ------------------ | | `agent` (default) | Adaptive | Adaptive | Automatic | Varies | **Most documents** | | `simple` | Fastest | Full | — | Lowest | Small inputs | | `parallel` | Fast | None | LLM merge | Medium | Speed priority | | `sequential` | Medium | Full | Context | Medium | Context-dependent | | `parallelAutoMerge` | Fast | None | Auto + dedupe | Medium | Large arrays | | `sequentialAutoMerge` | Medium | Full | Auto + dedupe | Medium | Ordered arrays | | `doublePass` | Slow | Full | LLM merge | High | Maximum quality | | `doublePassAutoMerge` | Slow | Full | Auto + dedupe | High | Quality + arrays | *** Agent (Default) [#agent-default] **The Agent strategy is the default.** You don't need to specify `--strategy agent` — it's used automatically when you run `struktur extract`. Autonomous extraction using a virtual filesystem. The agent decides when to read files, search for patterns, and build output incrementally. How it works [#how-it-works] 1. **Document loaded** into virtual filesystem (`/artifacts/artifact.json`, `/artifacts/manifest.json`, `/artifacts/images/`) 2. **Agent explores** using tools: read files, grep for patterns, list directories, execute commands 3. **Incremental extraction** — calls `set_output_data` when first data found, `update_output_data` as more discovered 4. **Validation** — schema validation on every output update, with automatic retry on errors 5. **Completion** — agent calls `finish` when done, or `fail` if extraction impossible The agent adapts to your document: * **Small documents** — reads everything at once * **Large documents** — navigates systematically, searching for relevant sections * **Complex schemas** — builds output incrementally, validating as it goes Configuration [#configuration] Example [#example] ```bash # Agent is the default — no --strategy needed struktur extract --input ./document.pdf \ --schema ./schema.json \ --model anthropic/claude-sonnet-4 # With max steps limit struktur extract --input ./document.pdf \ --schema ./schema.json \ --model anthropic/claude-sonnet-4 \ --max-steps 30 ``` ```ts import { extract, agent } from "@struktur/sdk"; const result = await extract({ artifacts, schema, strategy: agent({ provider: "anthropic", modelId: "claude-sonnet-4", maxSteps: 50, }), }); ``` When to use [#when-to-use] * **Always try agent first** — it's the default for a reason * Works well for most document types and sizes * Automatically adapts to document structure * Best for complex schemas with nested objects Model compatibility [#model-compatibility] The agent requires models that support tool/function calling: | Provider | Compatible Models | | --------- | ------------------------------------------------ | | Anthropic | Claude 3.5 Sonnet, Claude 3 Opus, Claude 3 Haiku | | OpenAI | GPT-4o, GPT-4 Turbo, GPT-4, GPT-3.5 Turbo | | Google | Gemini 1.5 Pro, Gemini 1.5 Flash | Some models claim tool support but don't work well with the agent. Avoid: GPT-4o-mini (inconsistent tool calling), older GPT-3.5 models, models without native function calling. Virtual filesystem [#virtual-filesystem] The agent has access to a virtual filesystem containing: * `/artifacts/artifact.json` — All artifacts in JSON format (images replaced by virtual paths) * `/artifacts/manifest.json` — Summary and metadata * `/artifacts/images/` — Extracted image files (when artifacts have embedded images) The agent can: * **Read** files with pagination (`offset`, `limit`) * **Grep** for patterns * **Find** files by name * **List** directories * **Bash** execute commands (on virtual filesystem only) Output management [#output-management] Special tools for building extraction output: * **`set_output_data(data)`** — Set initial output (first time data is found) * **`update_output_data(changes)`** — Merge changes into existing output * **`finish()`** — Complete extraction (only works if data validates) * **`fail(reason)`** — Mark extraction as impossible The agent is encouraged to update output continuously as it explores, not wait until the end. *** Simple [#simple] Single-shot extraction for small inputs. Use when the agent is overkill for tiny documents. Configuration [#configuration-1] Algorithm [#algorithm] 1. Build extraction prompt from artifacts + schema 2. Send to LLM 3. Validate output against the schema 4. Retry on validation failure (up to 3 attempts) 5. Return validated output Example [#example-1] ```bash struktur extract --input document.txt --schema schema.json --strategy simple ``` ```js import { extract, simple } from "@struktur/sdk"; import { openai } from "@ai-sdk/openai"; const result = await extract({ artifacts, schema, strategy: simple({ model: openai("gpt-4o-mini"), }), }); ``` When to use [#when-to-use-1] * Document fits within the model's context window (\~10k tokens) * Simple schema without nested arrays * Testing or prototyping * Speed is the priority * When you want predictable token costs (agent costs vary by document) *** Parallel [#parallel] Concurrent batch processing with LLM merge. Configuration [#configuration-2] Algorithm [#algorithm-1] 1. Split artifacts into batches (respecting `chunkSize` and `maxImages`) 2. Extract from each batch concurrently 3. Validate each batch output with retry 4. Send all partial results to `mergeModel` for LLM merge 5. Validate merged output 6. Return final result Example [#example-2] ```bash struktur extract --input large.pdf --schema schema.json --strategy parallel --model openai/gpt-4o-mini ``` ```js import { extract, parallel } from "@struktur/sdk"; import { openai } from "@ai-sdk/openai"; const result = await extract({ artifacts, schema, strategy: parallel({ model: openai("gpt-4o-mini"), mergeModel: openai("gpt-4o-mini"), chunkSize: 10000, concurrency: 3, }), }); ``` When to use [#when-to-use-2] * Speed is the top priority * Chunks are relatively independent * Many documents to process * Can accept potential loss of cross-chunk context * When agent costs are too high for your use case *** Sequential [#sequential] Process chunks in order with context preservation. Configuration [#configuration-3] Algorithm [#algorithm-2] 1. Split artifacts into batches 2. For each batch in order: * Build prompt including previous extraction result as context * Extract from batch * Validate with retry * Store result for next iteration 3. Return final result Example [#example-3] ```bash struktur extract --input report.pdf --schema schema.json --strategy sequential --model openai/gpt-4o-mini ``` ```js import { extract, sequential } from "@struktur/sdk"; import { openai } from "@ai-sdk/openai"; const result = await extract({ artifacts, schema, strategy: sequential({ model: openai("gpt-4o-mini"), chunkSize: 10000, }), }); ``` When to use [#when-to-use-3] * Context between chunks matters * Building data incrementally (e.g., accumulating line items) * Later sections reference earlier sections * Need better accuracy than parallel * Agent is making too many tool calls for your document structure *** Auto-Merge Strategies [#auto-merge-strategies] Strategies with "AutoMerge" in the name use schema-aware merge and deduplication. They're ideal for extracting arrays that may have duplicates across chunks. parallelAutoMerge [#parallelautomerge] Parallel extraction with schema-aware merge and deduplication. **Best for:** Array extraction from large inputs where speed matters. sequentialAutoMerge [#sequentialautomerge] Sequential extraction with schema-aware merge and deduplication. **Best for:** Ordered array extraction where context matters. doublePassAutoMerge [#doublepassautomerge] Double-pass extraction with schema-aware merge and deduplication. **Best for:** Large array extraction with maximum quality requirement. *** Choosing a Strategy [#choosing-a-strategy] **Start with the Agent.** It's the default because it works best for most documents. | Strategy | When to use | | --------------------- | ---------------------------------------------------------- | | `agent` (default) | **Start here** — autonomous exploration for most documents | | `simple` | Small input, fits in one context window, predictable costs | | `parallel` | Large input, order doesn't matter, speed priority | | `sequential` | Large input, context carries across chunks | | `parallelAutoMerge` | Large input with arrays — parallel + dedup | | `sequentialAutoMerge` | Large input with arrays — sequential + dedup | | `doublePass` | Quality matters, two-pass refinement | | `doublePassAutoMerge` | Quality + arrays + dedup | Quick decision flowchart [#quick-decision-flowchart] ```mermaid flowchart TD A[Start] --> B{Try Agent first?} B -->|Yes| C[Use agent — default] B -->|Need fixed costs| D{Input fits in context?} D -->|Yes| E[Use simple] D -->|No| F{Extracting arrays?} F -->|Yes| G{Cross-chunk context matters?} F -->|No| H{Cross-chunk context matters?} G -->|Yes| I[sequentialAutoMerge or doublePassAutoMerge] G -->|No| J[parallelAutoMerge] H -->|Yes| K[sequential or doublePass] H -->|No| L[parallel] ``` *** See also [#see-also] * [The Extraction Pipeline](/docs/explanation/pipeline) — where strategies fit * [Chunking & Token Budgets](/docs/explanation/chunking) — how batches are formed * [Validation & Retries](/docs/explanation/validation) — the retry loop # Validation & Retries Why validation inside the loop? [#why-validation-inside-the-loop] Without in-loop validation, you get JSON that may or may not match your schema. You then have to write error handling, decide whether to retry, and figure out how to feed errors back. Struktur does all of this for you. How the retry loop works [#how-the-retry-loop-works] 1. Send the extraction prompt to the LLM. 2. Validate the response against the schema. 3. If valid: return it. 4. If invalid: serialize the validation errors into an XML block, append it to the message thread as a user message, go to step 1. 5. After `maxAttempts` (default 3): throw. The model sees its own mistake and a structured description of it. This self-correction loop is why most extractions converge within 2 attempts. Schema design affects retry rate [#schema-design-affects-retry-rate] Well-constrained schemas fail less often. Tips: * Always use `additionalProperties: false`. * Use `required` arrays explicitly. * Prefer `enum` for categorical fields. * Use `format` (e.g., `date`, `email`) only when you need it — it adds validation surface. Observing retries with events [#observing-retries-with-events] Use the `onMessage` event to see when retries happen: ```js events: { onMessage: ({ role, content }) => { if (role === "user" && String(content).includes("validation-errors")) { console.log("Retry triggered"); } } } ``` Smart Validation for Multi-Step Strategies [#smart-validation-for-multi-step-strategies] When using parallel or sequential strategies, your data might be split across multiple chunks. For example, an invoice's price might appear on page 1, while the vendor name appears on page 5. If both fields are `required` in your schema, validating intermediate results would fail unnecessarily. How smart validation works [#how-smart-validation-works] Struktur uses **lenient validation** during intermediate extraction steps: * **Type errors** (`string` vs `number`) → Retry immediately * **Format errors** (invalid email) → Retry immediately * **Required field errors** → Allowed during intermediate steps * **All constraints** → Enforced on final validation This means the model can extract partial data without pressure to hallucinate missing required fields. The final validation ensures all required fields are present before returning. Opting into strict validation [#opting-into-strict-validation] Disable smart validation with the `strict` flag: ```js const result = await extract({ artifacts, schema, strategy: parallel({ model: openai("gpt-4o-mini"), mergeModel: openai("gpt-4o-mini"), strict: true, // Validate required fields on every step }), }); ``` Use `strict: true` when: * You know each chunk contains complete data * You want early failure on missing fields * You're debugging extraction issues See also [#see-also] * [The Extraction Pipeline](/docs/explanation/pipeline) — where validation fits * [Events & Observability](/docs/sdk/events) — the events API * [The Artifact Format](/docs/explanation/artifact-format) — schema format # What is Struktur? import { Card, Cards } from 'fumadocs-ui/components/card'; import { Callout } from 'fumadocs-ui/components/callout'; Struktur is an all-in-one tool for structured data extraction using an **autonomous agent**. It turns documents into validated, schema-typed JSON by having an LLM agent explore the content, decide what to read, and build the output incrementally. Why Struktur? [#why-struktur] Large document batches arrive with data locked in semi-structured text. Invoices need to flow into spreadsheets. Product datasheets need to become database rows. The tooling exists, but the orchestration overhead is disproportionate to the extraction task itself. Managed APIs charge per page, impose schema constraints, and require document uploads to external infrastructure. LLM SDKs provide raw model access but leave you to write chunking, validation, retries, and merging every time. Struktur fills the gap: a focused extraction engine with an **autonomous agent** that handles the orchestration so you can focus on the output. Why an Agent? [#why-an-agent] Traditional extraction strategies (simple, parallel, sequential) require you to choose the right approach upfront. The agent decides: * **When to read** — entire document or specific sections * **How to search** — grep for patterns, list directories, execute bash commands * **What to extract** — build output incrementally as it explores * **How to validate** — check against schema and retry automatically The agent adapts to your document. Small invoices get read in one shot. Large catalogs get navigated systematically. The result is better accuracy without configuration complexity. Why not managed APIs? [#why-not-managed-apis] | Limitation | Impact | | ------------------ | --------------------------------------- | | Per-page pricing | Does not scale for large batches | | Schema constraints | You work within their data model | | Document upload | Non-starter for confidential workloads | | Black-box behavior | Debugging extraction failures is opaque | Why not a plain LLM SDK call? [#why-not-a-plain-llm-sdk-call] A single `generateText()` call gives you: * No chunking for large documents * No retries on schema validation failure * No merging of multi-chunk results * No typed output inferred from your schema You write the same orchestration boilerplate every time. Struktur's agent packages that orchestration into a single, adaptive strategy. Design philosophy [#design-philosophy] **Agent-first, zero configuration.** The agent strategy is the default. It explores documents autonomously, deciding when to read, search, or extract. No need to pick chunk sizes or parallelism upfront. * **Autonomous exploration.** The agent uses a virtual filesystem to read files, grep for patterns, find files, and execute commands. It builds output incrementally as it discovers data. * **Shell-composable by default.** Reads stdin, writes stdout, speaks JSON. Integrates with `jq`, `find`, `curl`, and any tool in your pipeline. * **Validation in the loop.** Errors go back to the model, not to you. The retry loop means most extractions converge within two attempts. * **Schema-first.** You define the shape, Struktur guarantees it. * **Fields shorthand.** Skip the JSON Schema boilerplate with `--fields "title, price:number, status:enum{draft|live}"`. Trade-offs [#trade-offs] | Trade-off | Rationale | | ---------------------------------- | ---------------------------------------------------------------------------------- | | Requires tool-calling models | The agent needs models that support function calling (Claude, GPT-4, etc.) | | Depends on Vercel AI SDK providers | OpenAI, Anthropic, Google supported; self-hosted models need OpenAI-compatible API | | Token costs vary by document | The agent makes multiple tool calls; large documents cost more than small ones | A 10-second demo [#a-10-second-demo] ```bash struktur extract --input invoice.pdf \ --fields "number, vendor, total:number" ``` Expected output: ```json { "number": "1042", "vendor": "Acme Corp", "total": 2400 } ``` The agent reads the PDF, decides how to extract the fields, and returns validated JSON. What Struktur is NOT [#what-struktur-is-not] **It is not a general document conversion tool.** It parses files for extraction purposes, not for format conversion. It does not produce formatted output from documents. * **It is not a managed API.** It runs locally and calls your provider directly. * **It does not stream.** Input in, JSON out. * **It is not a general LLM orchestration framework.** For the full mental model, see [The Extraction Pipeline](/docs/explanation/pipeline). Who is it for? [#who-is-it-for] What is the Agent Strategy? [#what-is-the-agent-strategy] The agent strategy is the default and recommended way to use Struktur. It implements: * **Virtual filesystem tools** — read, grep, find, ls, bash * **Output management** — set\_output\_data, update\_output\_data, finish, fail * **Autonomous exploration** — the agent decides what to do based on your schema * **Incremental extraction** — builds output as it discovers data How it works [#how-it-works] 1. The agent receives your schema and access to a virtual filesystem containing the document 2. It can read files, search for patterns, list directories, and execute commands 3. As it finds data, it calls `set_output_data` or `update_output_data` to build the result 4. When complete, it calls `finish` to return validated JSON When to use other strategies [#when-to-use-other-strategies] The agent is the default and works best for most documents. However, other strategies are available for specific cases: | Strategy | When to use | | --------------------- | ------------------------------------------------------- | | `agent` (default) | Autonomous exploration — best for most documents | | `simple` | Small input that fits in one context window | | `parallel` | Large input where speed matters more than accuracy | | `sequential` | Large input where order matters | | `parallelAutoMerge` | Large arrays with parallel processing + deduplication | | `sequentialAutoMerge` | Large arrays with sequential processing + deduplication | | `doublePass` | Maximum quality with two-pass refinement | | `doublePassAutoMerge` | Maximum quality with arrays + deduplication | See [Extraction Strategies](/docs/explanation/strategies) for details on all strategies. Quick navigation [#quick-navigation] | Goal | Section | | ---------------------------------- | ------------------------------------------------------ | | New here? | [Quickstart](/docs/quickstart) | | Need to accomplish something? | [Examples](/docs/examples) | | Looking up a flag or type? | [CLI Reference](/docs/cli) | | Quick schema without writing JSON? | [Fields Shorthand](/docs/cli/fields) | | Want to understand how it works? | [Concepts](/docs/explanation) | | Parse files into artifacts? | [Document Parsing](/docs/explanation/document-parsing) | # Quickstart import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; Extract structured data from any file in 3 commands. About 5 minutes. **Using an AI assistant?** Point it at `https://struktur.sh/llms.txt` for LLM-optimized docs, or install the [Agent Skill](/docs/skill) for built-in Struktur knowledge. Prerequisites [#prerequisites] * Node.js 18+ or Bun installed * An API key from OpenAI, Anthropic, Google, OpenCode, or OpenRouter Install [#install] ```bash npm install -g @struktur/cli ``` ```bash bun install -g @struktur/cli ``` Verify: ```bash struktur --help ``` You should see the usage output. Configure your API key [#configure-your-api-key] Store your API key securely with the CLI: ```bash echo "sk-..." | struktur config providers add openai --token-stdin --default ``` The `--default` flag automatically queries the provider API and sets the cheapest available model as default, so `--model` becomes optional in all future commands. Output: `{ "provider": "openai", "stored": "keychain" }` (or `"file"` on Linux). Set a default model (if not using --default) [#set-a-default-model-if-not-using---default] ```bash struktur config models use openai/gpt-4o-mini ``` Once set, `--model` is optional in all `extract` commands. Extract your first data [#extract-your-first-data] ```bash echo "Invoice #1042 from Acme Corp. Total: $2,400.00. Due: April 1, 2026." | \ struktur --stdin \ --fields "invoice_number, vendor, total:number, due_date" \ --model openai/gpt-4o-mini ``` ```bash struktur --input invoice.pdf \ --fields "invoice_number, vendor, total:number, due_date" \ --model openai/gpt-4o-mini ``` The `--fields` flag builds a JSON Schema on the fly. Each field defaults to `string`; append `:number`, `:integer`, `:bool`, etc. to set the type. See [Fields Shorthand](/docs/cli/fields) for the full syntax. If you need more control (optional fields, nested objects), pass a full schema instead: ```bash struktur --input invoice.pdf \ --schema-json '{"type":"object","properties":{"invoice_number":{"type":"string"},"vendor":{"type":"string"},"total":{"type":"number"},"due_date":{"type":"string"}},"required":["invoice_number","vendor","total","due_date"],"additionalProperties":false}' \ --model openai/gpt-4o-mini ``` Notice that `total` is a number, not a string — Struktur enforced the schema. What happened? [#what-happened] For the text example, stdin input was loaded as an artifact, the LLM generated output, and it was validated against your schema. The `simple` strategy handled this in a single LLM call. For the PDF example, an extra step happened first: the built-in PDF parser extracted text (and optionally images) from the file and converted it into an artifact, then extraction proceeded as normal. Add `--images` to include embedded images, or `--screenshots` to render page screenshots. To understand what happened inside, read [The Extraction Pipeline](/docs/explanation/pipeline). Where to go next [#where-to-go-next] | Goal | Link | | ----------------------- | ------------------------------------------------------ | | Keep learning | [The Extraction Pipeline](/docs/explanation/pipeline) | | Solve a real problem | [Extract Invoice Data](/docs/examples/extract-invoice) | | Look up all CLI flags | [CLI Reference](/docs/cli/extract) | | Use it in TypeScript | [TypeScript SDK](/docs/sdk/installation) | | Understand file parsing | [Document Parsing](/docs/explanation/document-parsing) | # Events & Observability import { TypeTable } from 'fumadocs-ui/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; Available events [#available-events] void', required: false, }, onMessage: { description: 'Fired for every message in the LLM conversation', type: '(msg: { role: string; content: unknown }) => void', required: false, }, onProgress: { description: 'Fired when a strategy can report percentage progress', type: '(progress: number) => void', required: false, }, onTokenUsage: { description: 'Fired after each LLM call with token usage', type: '(usage: { inputTokens: number; outputTokens: number; totalTokens: number }) => void', required: false, }, }} /> Example: progress bar [#example-progress-bar] ```js const result = await extract({ artifacts, schema, strategy: parallel({ model, mergeModel: model, chunkSize: 8000 }), events: { onStep: ({ step, total, label }) => { process.stderr.write(`[${step}/${total ?? "?"}] ${label ?? "working"}\n`); }, onTokenUsage: ({ totalTokens }) => { process.stderr.write(`Tokens so far: ${totalTokens}\n`); }, }, }); ``` Example: observe retries [#example-observe-retries] ```js const result = await extract({ artifacts, schema, strategy: simple({ model }), events: { onMessage: ({ role, content }) => { if (role === "user" && String(content).includes("validation-errors")) { console.log("Retry triggered"); } }, }, }); ``` See also [#see-also] * [extract()](/docs/sdk/extract) — main extraction function * [Validation & Retries](/docs/explanation/validation) — validation concept # extract() import { TypeTable } from 'fumadocs-ui/components/type-table'; import { Callout } from 'fumadocs-ui/components/callout'; import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; Usage [#usage] ```js import { extract, simple } from "@struktur/sdk"; import { openai } from "@ai-sdk/openai"; const result = await extract({ artifacts, schema, strategy: simple({ model: openai("gpt-4o-mini") }), }); console.log(result.data); console.log(result.usage.totalTokens); ``` Options [#options] Exactly one of `schema` or `fields` must be provided. Passing both, or neither, throws immediately. Result [#result] Using fields instead of schema [#using-fields-instead-of-schema] For quick extractions where you don't need full JSON Schema control, pass a `fields` string: ```ts const result = await extract({ artifacts, fields: "title, author, year:integer, genre:enum{fiction|nonfiction|reference}", strategy: simple({ model: openai("gpt-4o-mini") }), }); ``` `fields` supports scalar types (`string`, `number`/`float`, `boolean`/`bool`, `integer`, `int`), enum sets (`status:enum{draft|live}`), and arrays (`tags:array{string}`). See the [Fields Shorthand](/docs/cli/fields) reference for the full syntax. See also [#see-also] * [Fields Shorthand](/docs/cli/fields) — quick schema syntax reference * [Installation & Setup](/docs/cli/installation) — getting started * [Extraction Strategies](/docs/explanation/strategies) — creating strategies * [Events & Observability](/docs/sdk/events) — progress tracking # SDK import { Card, Cards } from 'fumadocs-ui/components/card'; The Struktur SDK provides a TypeScript API for programmatic data extraction. # Installation import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; import { Card, Cards } from 'fumadocs-ui/components/card'; ```bash npm install @struktur/sdk ``` ```bash bun add @struktur/sdk ``` Peer dependency: `typescript ^5`. The package exports all types, functions, and strategy factories from the entrypoint. The `@ai-sdk/anthropic`, `@ai-sdk/google`, `@ai-sdk/openai`, and `@openrouter/ai-sdk-provider` packages are bundled as dependencies — you do not need to install them separately. Exports [#exports] ```js // Main function import { extract } from "@struktur/sdk"; // Strategy factories import { simple, parallel, sequential, parallelAutoMerge, sequentialAutoMerge, doublePass, doublePassAutoMerge } from "@struktur/sdk"; // Artifact helpers import { parse, fileToArtifact, urlToArtifact } from "@struktur/sdk"; ``` See also [#see-also] * [extract()](/docs/sdk/extract) — main extraction function * [parse()](/docs/sdk/parse) — creating artifacts # parse() The `parse()` function is the primary way to load files and text into artifacts. It handles MIME detection, parser resolution, and PDF image extraction automatically. ```typescript import { parse } from "@struktur/sdk"; const artifacts = await parse( { kind: "file", path: "document.pdf" }, { parserConfig: parsersConfig, // ParsersConfig — keyed by MIME type includeImages: true, // extract embedded PDF images screenshots: false, // render PDF page screenshots screenshotScale: 1.5, // scale factor for screenshots screenshotWidth: undefined, // target width in pixels (overrides screenshotScale) } ); ``` *** Input kinds [#input-kinds] | Input kind | Description | | -------------------------------------- | -------------------------------------------------------------------------------------- | | `{ kind: "text", text }` | Text artifact (split on double newlines) | | `{ kind: "file", path, mimeType? }` | File artifact — MIME auto-detected, parser resolved from `parserConfig` then built-ins | | `{ kind: "buffer", buffer, mimeType }` | Buffer artifact — parser resolved from `parserConfig` then built-ins | | `{ kind: "artifact-json", data }` | Validates and hydrates pre-built artifact JSON | *** Options [#options] | Option | Type | Default | Description | | ----------------- | --------------- | ------- | ---------------------------------------------------- | | `parserConfig` | `ParsersConfig` | `{}` | Custom parsers keyed by MIME type | | `includeImages` | `boolean` | `false` | Extract embedded images from PDFs | | `screenshots` | `boolean` | `false` | Render PDF page screenshots | | `screenshotScale` | `number` | `1.5` | Scale factor for screenshots | | `screenshotWidth` | `number` | — | Target width in pixels (overrides `screenshotScale`) | *** Custom Parsers [#custom-parsers] Pass a `parserConfig` to use custom parsers without CLI config: ```typescript import { parse } from "@struktur/sdk"; import type { ParsersConfig, InlineParserDef } from "@struktur/sdk"; import * as XLSX from "xlsx"; // npm package parser const parserConfig: ParsersConfig = { "application/vnd.ms-excel": { type: "npm", package: "@myorg/xlsx-parser", }, }; // or inline parser const inlineParserConfig: ParsersConfig = { "application/vnd.ms-excel": { type: "inline", handler: async (buffer) => { const workbook = XLSX.read(buffer); const contents = workbook.SheetNames.map((name, i) => ({ page: i + 1, text: XLSX.utils.sheet_to_csv(workbook.Sheets[name]), })); return { id: `excel-${crypto.randomUUID()}`, type: "file", raw: async () => buffer, contents, }; }, }, }; const artifacts = await parse( { kind: "file", path: "report.xlsx" }, { parserConfig: inlineParserConfig } ); ``` Inline parser signature [#inline-parser-signature] An inline parser is an async function that takes a Buffer and returns an Artifact: ```typescript type InlineParserHandler = (buffer: Buffer) => Promise; const myParser: InlineParserHandler = async (buffer) => { const pages = await parseMyFormat(buffer); return { id: `doc-${crypto.randomUUID()}`, type: "file", raw: async () => buffer, contents: pages.map((page, i) => ({ page: i + 1, text: page.text, media: page.images.map((img) => ({ type: "image", base64: img.base64, })), })), }; }; ``` *** Other Helpers [#other-helpers] urlToArtifact(url) [#urltoartifacturl] Fetches a URL and expects it to return pre-serialized artifact JSON. Validates and hydrates. ```js import { urlToArtifact } from "@struktur/sdk"; const artifacts = await urlToArtifact("https://example.com/artifact.json"); ``` parseSerializedArtifacts(text) [#parseserializedartifactstext] Parses a JSON string into artifacts with schema validation. validateSerializedArtifacts(data) [#validateserializedartifactsdata] Validates an already-parsed value against the artifact schema. hydrateSerializedArtifacts(items) [#hydrateserializedartifactsitems] Adds the `raw()` function to serialized artifacts. splitTextIntoContents(text) [#splittextintocontentstext] Splits a text string on double newlines into content slices. *** Deprecated: fileToArtifact [#deprecated-filetoartifact] `fileToArtifact` is deprecated. Use `parse()` with `InlineParserDef` in `parserConfig` instead. **Important:** `fileToArtifact` uses the legacy `providers` registry, which does not include the built-in PDF parser or any parsers configured via `config parsers add`. For PDF and other format support, use `parse` instead. ```js // Deprecated — use parse() instead import { fileToArtifact } from "@struktur/sdk"; import fs from "node:fs/promises"; const buffer = Buffer.from(await fs.readFile("document.txt")); const artifact = await fileToArtifact(buffer, { mimeType: "text/plain", providers: { /* deprecated */ } }); ``` *** See also [#see-also] * [The Artifact Format](/docs/explanation/artifact-format) — JSON spec * [Document Parsing](/docs/explanation/document-parsing) — how files are converted to artifacts and extending the parser system # Struktur Agent Skill import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; Struktur provides an Agent Skill that teaches AI coding assistants (like Claude Code, OpenCode, Codex, etc.) how to use Struktur effectively. What is an Agent Skill? [#what-is-an-agent-skill] An Agent Skill is a modular knowledge package that AI coding agents can discover and load automatically. Skills follow an open standard and work across 16+ AI agent tools. When you ask your AI agent to work with Struktur, the skill automatically loads and provides: * **API Usage**: How to use `extract()`, build artifacts, define schemas * **Strategy Selection**: When to use `simple`, `parallel`, `sequential`, `doublePass`, etc. * **Schema Definition**: JSON Schema patterns and shorthand field syntax * **CLI Commands**: All struktur CLI commands and options * **Best Practices**: Token budgets, validation retries, merge rules Why Use the Skill? [#why-use-the-skill] Without the skill, you'd need to explain Struktur's API, strategies, and best practices in every conversation. With the skill installed, your AI agent already knows: * Which strategy to use for your use case * How to define schemas correctly * How to configure the CLI * Common patterns and gotchas Supported Tools [#supported-tools] The skill works with any tool that supports the [Agent Skills open standard](https://github.com/anthropics/skills): | Tool | Support | Notes | | ----------------- | ------- | ---------------------------------------- | | Claude Code | ✅ | Full support with progressive disclosure | | OpenCode | ✅ | Full support | | OpenAI Codex | ✅ | Works alongside AGENTS.md | | Amp | ✅ | Full support | | VS Code (Copilot) | ✅ | Workspace-level installation | | Cursor | ✅ | Project-level installation | | Gemini CLI | ✅ | Full support | | JetBrains (Junie) | ✅ | IDE integration | And 10+ more tools. Package [#package] The skill is distributed as an npm package: `@struktur/skill` ```bash npm install @struktur/skill # or bun add @struktur/skill ``` Next Steps [#next-steps] # Installation import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; Install the Struktur agent skill for your preferred AI coding tool. Installation Methods [#installation-methods] ```bash # Install the package npm install @struktur/skill # or bun add @struktur/skill # Copy to your tool's skill directory cp -r node_modules/@struktur/skill/skills/struktur ~/.config/claude/skills/ ``` If you have the [skills CLI](https://github.com/vercel/skills) installed: ```bash # Install from npm npx skills add @struktur/skill # Or install from GitHub npx skills add https://github.com/mateffy/struktur/tree/main/packages/skill/skills/struktur ``` ```bash # Clone and copy git clone https://github.com/mateffy/struktur.git cp -r struktur/packages/skill/skills/struktur ~/.config/claude/skills/ ``` Tool-Specific Installation [#tool-specific-installation] **Global installation** (available in all projects): ```bash mkdir -p ~/.config/claude/skills cp -r node_modules/@struktur/skill/skills/struktur ~/.config/claude/skills/ ``` **Project installation** (only in this project): ```bash mkdir -p .agents/skills cp -r node_modules/@struktur/skill/skills/struktur .agents/skills/ ``` **Global installation**: ```bash mkdir -p ~/.config/opencode/skills cp -r node_modules/@struktur/skill/skills/struktur ~/.config/opencode/skills/ ``` **Project installation**: ```bash mkdir -p .agents/skills cp -r node_modules/@struktur/skill/skills/struktur .agents/skills/ ``` **Global installation**: ```bash mkdir -p ~/.codex/skills cp -r node_modules/@struktur/skill/skills/struktur ~/.codex/skills/ ``` **Project installation**: ```bash mkdir -p .codex/skills cp -r node_modules/@struktur/skill/skills/struktur .codex/skills/ ``` **Global installation**: ```bash mkdir -p ~/.config/amp/skills cp -r node_modules/@struktur/skill/skills/struktur ~/.config/amp/skills/ ``` **Project installation**: ```bash mkdir -p .agents/skills cp -r node_modules/@struktur/skill/skills/struktur .agents/skills/ ``` **Workspace installation**: ```bash mkdir -p .vscode/skills cp -r node_modules/@struktur/skill/skills/struktur .vscode/skills/struktur ``` **Project installation**: ```bash mkdir -p .cursor/skills cp -r node_modules/@struktur/skill/skills/struktur .cursor/skills/struktur ``` **Global installation**: ```bash mkdir -p ~/.config/gemini/skills cp -r node_modules/@struktur/skill/skills/struktur ~/.config/gemini/skills/ ``` **Project installation**: ```bash mkdir -p .agents/skills cp -r node_modules/@struktur/skill/skills/struktur .agents/skills/ ``` Verification [#verification] After installation, restart your AI agent and ask: ``` "Do you have access to the struktur skill?" ``` The agent should confirm it can see the skill and describe what it knows about Struktur. Updating [#updating] To update to the latest version: ```bash # Update the npm package npm update @struktur/skill # Re-copy to skill directory cp -r node_modules/@struktur/skill/skills/struktur ~/.config/claude/skills/ ``` Uninstalling [#uninstalling] Remove the skill directory: ```bash # For Claude Code rm -rf ~/.config/claude/skills/struktur # For OpenCode rm -rf ~/.config/opencode/skills/struktur # For project-level installations rm -rf .agents/skills/struktur ``` # Usage import { Callout } from 'fumadocs-ui/components/callout'; import { Card, Cards } from 'fumadocs-ui/components/card'; import { TypeTable } from 'fumadocs-ui/components/type-table'; import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; How to use the Struktur agent skill with your AI coding assistant. How It Works [#how-it-works] The skill uses **progressive disclosure** to minimize token usage: 1. **Metadata Phase** (\~100 tokens): Agent loads only the skill name and description 2. **Activation Phase**: When your request matches the description, the agent asks permission to load the full skill 3. **Full Load** (up to 5,000 tokens): Complete skill instructions load into context 4. **On-Demand**: Supporting files (scripts, references) load as needed This means you can have hundreds of skills installed without bloating the agent's context window. Triggering the Skill [#triggering-the-skill] The skill automatically activates when you mention Struktur-related tasks: ``` "Use struktur to extract product data from this PDF" "Set up struktur to parse invoices with parallel strategy" "Help me configure the struktur CLI for my project" "Import extract from @struktur/sdk and set up a schema" ``` The agent will recognize these requests and load the skill automatically. Example Conversations [#example-conversations] **You:** ``` Use struktur to extract the title and author from this document. ``` **Agent (with skill loaded):** ``` I'll use Struktur's simple strategy for this extraction. Let me: 1. Build an artifact from your document 2. Define a schema with title and author fields 3. Call extract() with the simple strategy [proceeds with correct API usage, proper schema definition, and best practices] ``` **You:** ``` I have a 500-page PDF with product listings. Extract all products with their names, prices, and categories. ``` **Agent (with skill loaded):** ``` For a 500-page document with multiple products, I'll use the parallel strategy: - It will chunk the document by token budget - Extract products from each chunk in parallel - Merge the results automatically Let me set this up with: - strategy: parallel with appropriate chunk size - schema: array of products with name, price, category - model: your configured default [proceeds with optimal configuration] ``` **You:** ``` Set up the struktur CLI to use OpenAI GPT-4o as the default model. ``` **Agent (with skill loaded):** ``` I'll configure the struktur CLI: 1. Set your OpenAI API token 2. Set gpt-4o as the default model 3. Optionally create aliases for quick access [runs correct CLI commands] ``` What the Skill Knows [#what-the-skill-knows] When loaded, the skill provides the agent with: API Knowledge [#api-knowledge] * How to import and use `extract()` * All strategy options and when to use them * Schema definition patterns (JSON Schema and fields shorthand) * Artifact building from files and URLs * Event hooks for monitoring progress Strategy Selection [#strategy-selection] The skill teaches the agent to choose strategies based on: | Input Size | Output Shape | Recommended Strategy | | --------------------- | ------------------- | --------------------------------- | | Small (`<10K` tokens) | Any | `simple` | | Large | Array/list | `parallel` or `parallelAutoMerge` | | Large | Object with context | `sequential` | | Large | Maximum accuracy | `doublePass` | CLI Commands [#cli-commands] All struktur CLI commands and options: * `struktur extract` with all flags * `struktur models` for model management * `struktur providers` for API key setup * Field shorthand syntax * Debug and logging options Best Practices [#best-practices] * Token budget recommendations * Validation retry patterns * Merge and deduplication rules * Schema strict mode usage * Common pitfalls and how to avoid them Manual Invocation [#manual-invocation] Some tools allow manual skill invocation: **Claude Code:** ``` /struktur ``` **OpenCode:** ``` /load-skill struktur ``` Check your tool's documentation for manual skill loading commands. Troubleshooting [#troubleshooting] If the skill doesn't load automatically: 1. **Check installation**: Verify the skill is in the correct directory 2. **Restart the agent**: Some tools require a restart after installation 3. **Be more specific**: Use explicit keywords like "struktur", "extract", "artifact" 4. **Check logs**: Some tools show skill discovery in debug mode If the skill seems outdated: ```bash # Update to latest version npm update @struktur/skill cp -r node_modules/@struktur/skill/skills/struktur ~/.config/claude/skills/ ``` The skill provides general guidance, but you can override: ``` Use struktur with the sequential strategy (not parallel) because order matters for this document. ``` Advanced Usage [#advanced-usage] Custom Instructions [#custom-instructions] You can add project-specific instructions alongside the skill: **In `.agents/AGENTS.md` or `CLAUDE.md`:** ```markdown When using struktur for this project: - Always use the parallel strategy for invoice extraction - Set chunkSize to 8000 for our PDF format - Use the "smart" model alias for better accuracy ``` Combining with Other Skills [#combining-with-other-skills] The Struktur skill works alongside other skills: Resources [#resources] * [Struktur Documentation](/docs) * [Agent Skills Specification](https://github.com/anthropics/skills) * [Skills CLI](https://github.com/vercel/skills) # What is an Extraction Agent? An extraction agent is an autonomous LLM that explores documents and decides how to extract data, rather than following a fixed extraction strategy. It uses tools to read, search, and navigate documents before producing output. How It Differs from Fixed Strategies [#how-it-differs-from-fixed-strategies] | Approach | How It Works | | ---------- | ---------------------------------------------------------- | | Simple | Process entire document in one LLM call | | Parallel | Split into chunks, process simultaneously | | Sequential | Process chunks in order, building up results | | **Agent** | Explore document, decide what to read, extract iteratively | Why Use an Agent? [#why-use-an-agent] Fixed strategies work well when you know the document structure upfront. But when documents vary: * **Unknown structure** — Agent discovers layout dynamically * **Variable length** — Agent reads only what's needed * **Complex navigation** — Agent can search, skip, revisit sections * **Adaptive extraction** — Agent adjusts strategy per document How Agents Work [#how-agents-work] An extraction agent is given: 1. **A virtual filesystem** — Access to document content 2. **Tools** — Read, grep, find, explore 3. **Output schema** — What data to extract 4. **Control tools** — Set/update output, finish, fail The agent: 1. Explores the document using tools 2. Identifies relevant sections 3. Extracts data iteratively 4. Validates and corrects 5. Signals completion Example: Contract Analysis [#example-contract-analysis] ``` Agent: "I need to find the parties involved." → uses grep("party") → finds section 2.1 Agent: "Let me read section 2.1" → uses read("/artifacts/contract.pdf#section-2.1") → extracts party names Agent: "Now I need the effective date" → uses grep("effective date") → extracts date Agent: "I have all required fields" → uses finish() ``` Trade-offs [#trade-offs] | Advantage | Disadvantage | | ----------------------------- | --------------------------- | | Handles unknown structures | Variable token cost | | Adapts to document variations | Requires tool-calling model | | Can skip irrelevant sections | More complex to debug | | Better for complex documents | Overkill for simple cases | When to Use an Agent [#when-to-use-an-agent] Use an agent strategy when: * Document structure varies significantly * You don't know what sections contain relevant data * Documents are long but only parts are relevant * You need to cross-reference within the document Use simpler strategies when: * Documents have consistent structure * Entire document is relevant * You know exactly what to extract See Also [#see-also] * [What is Structured Data Extraction?](/docs/what-is-structured-data-extraction) * [Choosing an Extraction Strategy](/blog/agent-vs-simple-vs-parallel) * [Struktur Documentation](/docs) # What is Structured Data Extraction? Structured data extraction is the process of converting unstructured documents (PDFs, images, text files) into validated, typed data using AI and schema validation. The output is typically JSON that conforms to a predefined schema. The Problem [#the-problem] Documents contain valuable information, but it's locked in unstructured formats: * **PDFs** — Text, tables, images mixed together * **Images** — Scanned documents, photos, screenshots * **Text files** — Unstructured prose, logs, transcripts Traditional approaches require manual data entry or brittle regex patterns that break when formats change. The Solution [#the-solution] Modern structured data extraction uses LLMs to: 1. **Parse** documents into processable content 2. **Extract** relevant information based on a schema 3. **Validate** output against the schema 4. **Retry** with error feedback if validation fails Key Components [#key-components] | Component | Purpose | | --------------- | ----------------------------------------------- | | Document parser | Converts PDFs, images to text/structure | | Schema | Defines expected output structure (JSON Schema) | | LLM | Extracts data following the schema | | Validator | Checks output against schema | | Retry loop | Fixes errors with LLM feedback | Use Cases [#use-cases] * **Invoice processing** — Extract vendor, line items, totals * **Contract analysis** — Extract parties, dates, obligations * **Form data entry** — Convert scanned forms to structured data * **Research papers** — Extract methods, results, citations Tools for Structured Data Extraction [#tools-for-structured-data-extraction] * **Struktur** — Open source, autonomous agent-based extraction * **LlamaExtract** — Managed cloud service with citations * **Unstract** — Open source with visual prompt engineering * **Instructor** — Python library for structured LLM outputs See Also [#see-also] * [What is an Extraction Agent?](/docs/what-is-an-extraction-agent) * [Struktur vs Alternatives](/compare) * [Quickstart Guide](/docs/quickstart)