@eigenpal/cli 0.17.5 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +201 -17
- package/dist/docs-bundle.json +26 -5
- package/dist/skill/reference/step-types.md +265 -0
- package/dist/skill/reference/workflow-yaml.md +1 -0
- package/package.json +1 -1
package/dist/docs-bundle.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"title": "Changelog",
|
|
21
21
|
"description": "Production release history for the EigenPal platform.",
|
|
22
22
|
"source": "public",
|
|
23
|
-
"content": "---\ntitle: Changelog\ndescription: Production release history for the EigenPal platform.\n---\n\nEigenPal ships on a unified platform version. Each production release updates Studio, the public API, infrastructure, and (when applicable) the CLI and SDKs together.\n\n{/* GENERATED:CHANGELOG_INDEX START */}\n\n## 0.17.5 (2026-09-21)\n\n### Platform (Studio)\n\n- Summary Agent: spreadsheet uploads now use a hardened parser that prevents crafted workbook content from causing excessive processing or modifying object prototypes.\n\n### CLI\n\n- CLI skill reference documents the new `ai.figure-crops` workflow step for cropping and captioning figures from parsed PDFs.\n\n### SDK\n\n- Python SDK: installs now require `anyio` 4.14.2 or newer. Earlier versions encoded host names with IDNA 2003 when opening a TLS connection, so a crafted host name could be matched against the wrong certificate. The TypeScript SDK is unaffected.\n\n---\n\n## 0.17.4 (2026-09-18)\n\n### Platform (Studio)\n\n- Strengthen organization access controls so role permissions, API-key scopes, and member removal apply consistently across Studio and the API. Webhook configuration and signing secrets are restricted to organization owners and admins. Service credentials cannot exceed member permissions, while members retain file and review editing through the API.\n- Organization owners can invite new users directly as owners. Owner invitations can only be created and managed by existing owners.\n\n### Public API\n\n- Strengthen organization access controls so role permissions, API-key scopes, and member removal apply consistently across Studio and the API. Webhook configuration and signing secrets are restricted to organization owners and admins. Service credentials cannot exceed member permissions, while members retain file and review editing through the API.\n\n### Infrastructure & processing\n\n- Cloud workflow HTTP and website-reader actions now restrict requests to public HTTP/S destinations, verify TLS for HTTPS requests, and reject HTTPS-to-HTTP redirects. Responses are limited to 20 MiB and requests to 120 seconds. Single-tenant installations retain private-network integration support.\n\n---\n\n## 0.17.3 (2026-09-18)\n\n### Public API\n\n- Large PDF parses are now more reliable under parallel load. The OCR service paces concurrent pages per document so multi-page jobs no longer exhaust inference memory and retry, which previously stalled big documents behind repeated restarts.\n\n---\n\n## 0.17.2 (2026-09-17)\n\n### Platform (Studio)\n\n- Settings gains a Data retention section for workspace admins: see how much file storage past and present runs consume, choose how long eval and other runs are kept, preview what falls past the horizon, and delete those runs in capped passes until clear. Runs that are still active are always kept. Enforcement offers two modes: purge input files only (run history and outputs stay, freeing the bulky source documents while keeping the audit trail) or delete full runs. Storage usage splits run inputs from outputs so admins can see what input-only cleanup would free.\n\n### Public API\n\n- Rerunning a run whose input files were removed by data retention now returns a clear `409 inputs-purged` error instead of failing while copying the missing files. Run history and outputs remain available.\n\n---\n\n## 0.17.1 (2026-09-15)\n\n- Internal improvements.\n\n---\n\n## 0.17.0 (2026-09-15)\n\n### Platform (Studio)\n\n- Studio can connect to a live agent inference session even when that session runs in a local sandbox instead of the hosted cloud environment.\n\n### Infrastructure & processing\n\n- Agent inference can now run in a local container sandbox, so on-prem and air-gapped deployments no longer need a cloud sandbox provider.\n- Agent runs can now execute in customer-hosted sandbox environments instead of only the Eigenpal cloud, and local Docker sandboxes connect reliably on plain localhost setups.\n\n---\n\n## 0.16.13 (2026-09-14)\n\n- Internal improvements.\n\n---\n\n## 0.16.12 (2026-09-14)\n\n### Platform (Studio)\n\n- Studio can import large dataset ZIP archives without taking the app down, including archives whose paths use Windows-style backslashes. Opening a long-running workflow no longer pulls every step’s full input and output at once. Run pages stay available, still show the steps timeline when phase data is missing, and keep the last loaded view if a refresh fails — with a retry control instead of a blank error page.\n\n### Public API\n\n- `GET /api/v1/runs/\\{id}?expand=execution` and `GET /api/v1/runs/\\{id}/steps` now return slim step rows (identity, status, timing, and a capped error excerpt) instead of inlining every step’s input and output. Inspect one step’s full payloads with the new `GET /api/v1/runs/\\{id}/steps/\\{stepExecutionId}` endpoint.\n\n### CLI\n\n- `eigenpal runs get` lists slim step rows by default. Pass `--include input,output` or `--step` to fetch full payloads for the steps you care about.\n\n### SDK\n\n- The TypeScript and Python SDKs now include `GET /api/v1/runs/\\{id}/steps/\\{stepExecutionId}` so you can load one step’s full input and output after listing slim run steps.\n\n---\n\n## 0.16.11 (2026-09-11)\n\n### Platform (Studio)\n\n- Studio: organization switching and other labeled menus now open reliably without a client-side error.\n\n---\n\n## 0.16.10 (2026-09-10)\n\n### Infrastructure & processing\n\n- OCR processing now distinguishes temporary host availability issues from true processing timeouts, keeps redundant production capacity warm, and pages only when unfinished work is affected. Short self-healing interruptions remain visible for diagnosis without generating misleading recovery alerts.\n\n---\n\n## 0.16.9 (2026-09-10)\n\n### Platform (Studio)\n\n- Harden web image handling, document image processing, and email delivery against crafted inputs that could cause remote code execution, excessive resource use, or recipient-validation bypasses.\n\n### Infrastructure & processing\n\n- Harden web image handling, document image processing, and email delivery against crafted inputs that could cause remote code execution, excessive resource use, or recipient-validation bypasses.\n\n---\n\n## 0.16.8 (2026-09-10)\n\n### Platform (Studio)\n\n- Files are easier to recognize throughout Studio with refreshed Office logos, dedicated PDF, archive, text, JSON, and code treatments, plus image thumbnails. File inputs now use a clearer rounded drag-and-drop experience with consistent selected-file previews.\n\n---\n\n## 0.16.7 (2026-09-10)\n\n### Platform (Studio)\n\n- File uploads now infer supported document media types from the filename when clients provide an empty or generic content type, allowing original Outlook messages and macro-enabled workbooks to upload without conversion.\n\n### Public API\n\n- File uploads now infer supported document media types from the filename when clients provide an empty or generic content type, allowing original Outlook messages and macro-enabled workbooks to upload without conversion.\n\n### CLI\n\n- The CLI now uploads macro-enabled Excel workbooks with the correct media type, so original `.xlsm` workflow inputs work without manual conversion.\n\n### SDK\n\n- TypeScript SDK file helpers now infer the correct media types for `.xlsm` workbooks and `.msg` Outlook messages.\n\n### Infrastructure & processing\n\n- Document parsing now reads Excel, legacy Excel, and macro-enabled workbooks directly, preserving every non-empty sheet and its cell evidence without executing formulas or macros. Large single-page extractions are also split into bounded AI context windows instead of failing at the model limit.\n\n---\n\n## 0.16.6 (2026-09-09)\n\n### Infrastructure & processing\n\n- Template processing now enforces memory limits when joining large collections, preventing crafted templates from exhausting worker memory.\n\n---\n\n## 0.16.5 (2026-09-08)\n\n### Platform (Studio)\n\n- Human review is now available in production, including workflow authoring and review queues.\n\n### Public API\n\n- Human review API endpoints are now available in every production organization for listing tasks, confirming fields, and completing reviews.\n\n### Infrastructure & processing\n\n- Workflow and agent runs can now request human review in every production organization.\n\n---\n\n## 0.16.4 (2026-09-08)\n\n### Platform (Studio)\n\n- Studio adds List Archive and Extract Archive File steps for ZIP workflow inputs. Enumerate archive contents, wire `\\{\\{ item.path }}` through foreach, and pass extracted files straight into Parse, Vision, or Search Files without leaving the builder.\n- Studio now finishes multi-GB file uploads with resumable multipart transfers, retrying individual parts without restarting the whole file.\n- Studio workflows now include Search Files: ask a question about a ZIP archive and get a cited answer plus the files the step inspected. Scope the search to a folder inside the archive and use the same model and reasoning controls as other AI steps.\n\n### Public API\n\n- Upload ZIP archives as reusable workflow inputs through the Files API. Archives can be much larger than single documents, so teams can pass bundled evidence, exports, or multi-file drops into the same runs that already accept PDFs and Office files.\n- File uploads can now negotiate a resumable multipart session for multi-GB objects, with on-demand part URLs, authoritative resume listing, and safe completion that never overwrites a finished file.\n- Starting a run from a reusable file id now keeps a private run-owned reference to the original bytes instead of duplicating large uploads. Deleting the reusable file leaves past runs readable until those runs themselves are deleted.\n\n### CLI\n\n- Dataset import and export now keep a single `/api/v1` prefix when the CLI base URL already includes `/api/v1`, so those commands reach the dataset API instead of a doubled `/v1/v1` path.\n- The CLI now uploads large run files with resumable, storage-direct transfers, so multi-GB inputs stream from disk instead of loading into memory or a single request.\n- The CLI now recognizes `.zip` files when uploading reusable workflow inputs, so archive uploads get the correct content type without extra flags.\n\n### SDK\n\n[View full release notes →](/releases/0.16.4)\n\n---\n\n## 0.16.3 (2026-09-08)\n\n### Platform (Studio)\n\n- Human review: open pending reviews directly from run details and confirm unchanged fields inline from the field list.\n\n### Public API\n\n- Human review field confirmations now preserve valid JSON `null` values and return clear validation errors when a reviewed value does not match its schema.\n\n---\n\n## 0.16.2 (2026-09-08)\n\n### Platform (Studio)\n\n- Human review: pause workflows and agents only when selected fields need attention, inspect source files beside structured output with spatial citation highlights when lineage is available, correct and confirm values, then approve the same run to continue. Confidence thresholds, explicit field selection, required-field progress, rejection, and cancellation keep exception review focused instead of forcing teams to recheck every field.\n\n### Public API\n\n- Human review APIs now list pending tasks, return review data and source-file references, persist field corrections and confirmations, and approve or reject paused runs with optimistic concurrency protection. Task detail may also include resolved lineage and parsed-document context when an upstream grounded extract produced sidecars.\n\n### CLI\n\n- Workflow authoring now includes human-review guidance and ready-to-use templates for confidence-based exception review and explicit approval of critical fields.\n\n### SDK\n\n- TypeScript and Python SDKs now expose human-review resources for listing and retrieving tasks, confirming corrected fields, downloading attached source files, and approving or rejecting paused runs.\n\n### Infrastructure & processing\n\n- Grounded extraction now runs the same shared algorithm locally when a parse step supplies a valid structured document and your provider supports strict structured output. Step JSON stays schema-shaped with `_grounding` metadata; lineage and the parsed document graph are stored as optional sidecars for review and audit, not mixed into workflow output. Text-only inputs and unsupported providers continue to use the existing text grounding path with clear degraded markers when the shared path is unavailable.\n\n---\n\n## 0.16.1 (2026-09-07)\n\n### Platform (Studio)\n\n- GPT-6 Astra is available across workflows, agents, evaluations, and vision tasks, with configurable reasoning and current token pricing.\n- Self-hosted PaddleOCR models now appear with clear provider identity in Studio model selectors and settings.\n- Archive imports now reject malformed ZIP64 data safely instead of risking an unbounded processing loop.\n\n### CLI\n\n- CLI archive handling now rejects malformed ZIP64 data safely instead of risking an unbounded processing loop.\n\n### Infrastructure & processing\n\n- GPT-6 Astra is available across workflows, agents, evaluations, and vision tasks, with configurable reasoning and current token pricing.\n- Self-hosted deployments can connect directly to PaddleOCR-VL for high-quality document parsing, with bounded concurrency and long-running PDF support built in.\n\n---\n\n## 0.16.0 (2026-09-07)\n\n### Platform (Studio)\n\n- Agent model pickers are now compact, searchable, grouped by provider, and ordered from the most capable model to the least without changing configured defaults.\n- Send Email is now a first-class workflow action. Add Resend or SMTP servers in Settings → Email servers, then pick a stable `ems_…` server on the step — including generated files from the same run as attachments. Cloud and on-prem share this Settings surface; there is no default server. Secrets stay encrypted, and reads never show credentials. Changing an SMTP host, username, or provider asks you to re-enter the password so a stored secret cannot follow a new destination.\n\n### Public API\n\n- The public API now manages organization email servers at `/v1/email-servers`: list, create, inspect, update, delete, and send a connectivity test. Each server is an `ems_…` resource with a Resend or SMTP transport. Secrets are encrypted and never returned; disabled servers reject tests, and names must be unique among live servers. Updating SMTP to a different host or username requires a new password or explicitly clearing authentication — the previous password is never reused against a new destination.\n\n### CLI\n\n- Manage organization email servers with `eigenpal email-servers`: list and inspect `ems_…` servers, create or update Resend and SMTP providers (including an optional SMTP CA PEM), delete servers, and send a live test message with `--to`. Secrets are read from stdin, a file, or a secure prompt — never from command-line flags.\n\n### SDK\n\n- TypeScript and Python SDKs now manage organization email servers: list, create, inspect, update, delete, and send a connectivity test. Stored credentials are never returned.\n\n### Infrastructure & processing\n\n- Send Email delivers through the organization server selected on the step. There is no fallback: a missing, deleted, or disabled server fails the run. Evaluation runs skip delivery, including workflows invoked from an eval. Cancelling a run aborts an in-flight send. Multi-tenant SMTP servers must resolve to public addresses, while single-tenant deployments can use internal relays. Partial SMTP acceptance reports which recipients were delivered or rejected so operators can avoid duplicate reruns. SMTP may still deliver twice if a worker stops after the relay accepts the message; Resend deduplicates the same step attempt.\n\n---\n\n## 0.15.1 (2026-09-04)\n\n### Infrastructure & processing\n\n- OpenParser OCR capacity now recovers cleanly after failed updates, avoiding unnecessary warm resources while preserving active document processing.\n\n---\n\n## 0.15.0 (2026-09-04)\n\n### Platform (Studio)\n\n- Workflow validation now explains when a catalog model belongs in `provider` instead of the raw `model` override, and reruns preserve clean original filenames in generated outputs and citations.\n- Make document inputs uploaded in Studio datasets available to experiment runs, including examples saved by earlier releases.\n\n### Public API\n\n- Experiment result responses and JSON exports now include evaluator-specific structured details, so clients can identify the exact JSON path, expected value, and actual value behind a failed comparison. CSV exports retain their existing stable columns.\n\n### CLI\n\n- Experiment analysis is now built into the CLI for workflows and agents: summarize pass, fail, error, and score totals; filter failures by evaluator; inspect structured discrepancy paths; and compare actual outputs between batches. Run payloads can be projected with `--select`, and repository-level validation discovers nested workflow projects automatically. Polling commands also expose consistent wait limits and reject flag combinations that cannot produce a reliable verdict. Waiting commands now keep human output concise by default; pass `--json` whenever scripts need the terminal payload.\n- `eigenpal docs` now lists, searches, and reads the complete release-matched platform documentation offline, including detailed evaluator syntax, workflow step guides, CLI reference, and the OpenAPI contract.\n- CLI file uploads now send valid content types for Outlook and unknown files, cancellation confirms the asynchronous request clearly, and run comparisons show every repeated workflow step by branch and iteration path.\n\n### Infrastructure & processing\n\n- Make document inputs uploaded in Studio datasets available to experiment runs, including examples saved by earlier releases.\n- Exact-diff evaluators can exclude volatile fields with `ignore: true`, including nested fields inside a selected object or array. This keeps golden outputs readable while scoring only stable business data such as identities, names, and classifications.\n- Script steps now bind inputs by parameter name, so workflows behave consistently after storage even when JSON key order differs from the authored YAML.\n\n[View full release notes →](/releases/0.15.0)\n\n---\n\n## 0.14.3 (2026-09-04)\n\n### Platform (Studio)\n\n- Model selection now includes the GPT 5.6 Sol, Terra, and Luna lineup plus Claude Fable 5, Fable 5.1, Opus 5, and Sonnet 5. Reasoning effort can be configured for supported workflow steps, agent training and runs, workflow-building chat, and LLM judges; existing choices keep their current provider defaults when the setting is omitted.\n\n### CLI\n\n- Workflow YAML can configure model-aware reasoning effort on AI steps and LLM judges. Omit the setting to preserve each model's current default.\n\n### Infrastructure & processing\n\n- AI processing supports the latest GPT and Claude request formats, including reliable structured extraction with Claude Fable 5 and Fable 5.1. Workflow and agent execution can pass an optional model-aware reasoning effort while preserving previous defaults, and agent sandboxes can use GPT 5.6, Claude Fable 5, Fable 5.1, Opus 5, and Sonnet 5.\n\n---\n\n## 0.14.2 (2026-09-02)\n\n### Infrastructure & processing\n\n- Single-node deployments can now upgrade installations with large legacy datasets without startup failures or loss of trigger configuration.\n\n---\n\n## 0.14.1 (2026-09-02)\n\n### Infrastructure & processing\n\n- Single-node deployments can now upgrade installations with legacy run review data without startup failures.\n\n---\n\n## 0.14.0 (2026-09-02)\n\nThis release gives document teams more control over structured-data quality.\nExact-diff evaluators can express field-specific matching rules, PDF parsing can\npreserve visual layout, and spreadsheet conversion can retain the rows, columns,\nand displayed values that matter to downstream workflows.\n\nCLI authoring guidance makes these capabilities easier to discover and safer to\nautomate, with non-interactive commands failing clearly instead of waiting on\nunavailable prompts.\n\n### Platform (Studio)\n\n- `ai.parse` adds `outputFormat: layout` for native PDF extraction. It returns fixed-width plain text that preserves columns, spacing, and line breaks for multi-column forms and tables that lose meaning when flattened. This PDF-only format must be selected explicitly with `parseMode: native` or `parseMode: native-or-ocr`: native mode fails closed if layout extraction cannot complete, while hybrid mode keeps accepted layout pages and substitutes OCR markdown only for suspect or empty pages. Existing formats and defaults are unchanged.\n- Spreadsheet to JSON now handles `.xls` and `.xlsx` workbooks with configurable headerless input or an explicit header row, named or positional column projection, rectangular ranges, and raw or displayed cell values. Displayed values preserve formatted text such as dates, leading zeros, punctuation, diacritics, and embedded newlines. Blank-cell and blank-row policies make sparse sheets predictable, while optional sheet metadata and diagnostics explain the selected range and non-fatal issues. Existing zero-config output remains `rows`, plus `fileId` when CSV output is enabled; zero-config CSV keeps the full selected sheet, and configured CSV follows the same projection. File, row, column, and cell limits reject unsafe workloads before conversion.\n- Exact-diff evaluators add composable per-field rules for extraction outputs. Choose ordered or unordered comparison independently from `items: at-least | at-most | exactly`, set numeric tolerances, and reject unexpected object fields where strict output shapes matter. Ordered arrays retain prefix-compatible matching, while unordered arrays find the best one-to-one structural pairing instead of depending on row order. Unordered object arrays can also use typed `matchBy` identities such as `sku` or `[country, sku]` for predictable single- or composite-key matching. Studio now provides a rule editor with path-aware validation. Grounding metadata is excluded from scoring, oversized comparison workloads fail safely with bounded diagnostics, and existing evaluator configurations keep their historical behavior.\n\n### Public API\n\n- Template creation now accepts reusable file references correctly, restoring CLI template uploads and workflow pushes that use local DOCX or XLSX files.\n\n### CLI\n\n[View full release notes →](/releases/0.14.0)\n\n---\n\n## 0.13.2 (2026-09-01)\n\n### CLI\n\n- CLI installation no longer downloads packages already included in its self-contained executable, reducing install overhead and improving compatibility with restricted enterprise networks.\n\n---\n\n## 0.13.1 (2026-08-31)\n\n### Platform (Studio)\n\n- Workflows can turn JSON rows into a downloadable Excel workbook with a new JSON to XLSX step. Pick ordered columns, map an array of objects, and get a `.xlsx` run output — including multi-sheet workbooks — without filling a template.\n- Studio now uses the shared theme provider, so system / light / dark stays aligned with OpenParser instead of drifting independently.\n- Publishing a workflow that still has a local `template: ./file.xlsx` path now fails with a clear error. Use `eigenpal workflow push` to upload the file, or set `templateId` to a `tmpl_…` id first.\n- Studio template uploads and replacements now create immutable, checksum-backed revisions and discover tokens consistently across DOCX and XLSX files. Deleting a logical template preserves revision content used by pinned workflows.\n- Fill Template (`transform.template`) now documents workspace `tmpl_...` templates separately from Git agent templates, and Studio copy covers DOCX and XLSX. XLSX files use `\\{placeholder}` and `\\{table:array.prop}` in the spreadsheet; `\\{\\{placeholder}}` in an uploaded sheet warns on upload and fails at fill instead of leaving leftover braces. `templateId` must be a workspace `tmpl_...` ID, not a file ID. Filled values that look like formulas stay text; formulas already in the template stay formulas.\n- Studio now blocks crafted HTML from retaining executable content during sanitization.\n\n### Public API\n\n- Workflow push can fully remove a template it uploaded if publishing then fails. Deleting a template that workflows already pin still keeps those revisions runnable.\n- The public API now lists the text, vision, and OCR models configured for your environment. `GET /v1/models` is a cheap catalog inventory for the current tenant: it reports whether each model is configured and which capability it is the default for, without probing live providers or exposing secrets.\n- The public API now supports the full lifecycle for tenant-scoped DOCX and XLSX templates: upload, inspect, list, download, replace, and delete, with direct-upload support for larger files. Every replacement creates a checksum-backed immutable revision; deleting the logical `tmpl_…` resource preserves revisions already pinned by workflows.\n\n### CLI\n\n- `workflow versions create --file` now resolves local `template:` paths the same way as `workflow push`, and `workflow templates smoke --data` help documents the JSON fixture file path.\n- `eigenpal workflow push` keeps local Office templates inside the workflow project, including when a path uses `../` or a symlink. Files outside that directory need `--allow-external-templates`. Push validates the workflow and version before uploading, creates new template resources instead of moving shared `tmpl_…` pointers, and fully removes those unpublished uploads if publish does not succeed.\n- When resolving a workflow by `wf_…` id, connection failures, auth errors, and server outages now surface their normal CLI messages instead of being misreported as \"workflow not found.\"\n\n[View full release notes →](/releases/0.13.1)\n\n---\n\n## 0.13.0 (2026-08-25)\n\n### Platform (Studio)\n\n- API reference for listing dataset examples now documents `include=metadata`, so you can page through example ids, names, and file refs without loading every payload.\n- Local Studio development no longer keeps a growing on-disk compiler cache that can exhaust memory during long `bun dev` sessions.\n- Studio's sidebar theme picker now supports keyboard navigation across system, light, and dark.\n- Studio dashboard sessions continue to authenticate `/api/v1` calls. Token-based `/api/v1` and `/v1` requests still work without a browser login, and the dedicated API hostname keeps its public routing and CORS behavior.\n- Studio save-as-release still publishes a tagged version as the live workflow. Publishing from an existing snapshot creates a new tagged release instead of rewriting the original tag, so earlier releases stay intact. Connect API docs version pickers follow the public version list, so after restore the current untagged version shows as latest while tagged releases stay selectable.\n\n### Public API\n\n- List dataset examples without pulling every payload. `GET /api/v1/automations/:id/examples` now accepts `include=metadata`, which returns ids, names, annotations, and expected file refs while leaving input and expected JSON unloaded. Keep the default `include=full` when you still need the complete example bodies.\n- You can now create, promote, and restore workflow versions through the public API. Create a tagged candidate with `activate: false` to test it without changing live traffic, then promote that exact version after it qualifies. You can also publish from YAML, copy an existing version into a new release without changing its original tag, or restore an earlier snapshot. Name-conflict errors include `conflictingWorkflowId` so you can see which workflow already owns the name.\n\n### CLI\n\n- `eigenpal workflow versions` can now create, promote, and restore YAML workflow versions through the public API. Tag a candidate from a YAML file or an existing snapshot with an explicit semver, keep it off live traffic with `--no-activate`, then promote that exact version when it qualifies. Restoring still rolls a workflow back by copying a snapshot into a new current version — lists now show that untagged HEAD clearly instead of hiding it. `workflow versions list --limit` and `--offset` page that full public list locally. `--json` prints the sliced `\\{ data, total, limit, offset }` envelope (`total` is the unsliced count), not the raw server payload.\n\n### SDK\n\n- TypeScript and Python SDKs can list dataset examples with `include: \"metadata\"`. That returns ids, names, annotations, and expected file refs without loading each example's input and expected JSON. The default remains a full payload, so existing list calls stay unchanged.\n\n[View full release notes →](/releases/0.13.0)\n\n---\n\n## 0.12.2 (2026-08-21)\n\n### Platform (Studio)\n\n- Document editing: the DOCX editor used for templates, run file review, and Office file previews now follows your light or dark theme, and Save (including Cmd/Ctrl+S) writes back to the document you opened instead of downloading a copy. Documents that fail to open now report the reason instead of showing an empty editor. Placeholder highlighting in DOCX templates is temporarily unavailable.\n- Connect: a workflow that wraps another workflow now documents the full response shape. Its OpenAPI spec, Swagger UI preview, and the response examples and generated types on the Connect page show the nested objects and arrays returned by the workflow it calls, instead of leaving those fields untyped.\n\n---\n\n## 0.12.1 (2026-08-19)\n\n### Platform (Studio)\n\n- Connect: download an OpenAPI spec (swagger.json) and preview it in Swagger UI directly from the Connect page. Selecting a workflow or agent produces a spec for just that automation: the five endpoints needed to integrate it (start a run, poll it, cancel it, list and download artifacts), with the request body documenting its exact input fields and file uploads and run responses typed with its actual output schema. With no selection you get the full platform API spec. Swagger UI is bundled with the product, so the preview also works in on-prem installs without internet access.\n\n---\n\n## 0.12.0 (2026-08-18)\n\n### Platform (Studio)\n\n- The workflow builder chat honors the new `disableTemperature: true` model configuration option, so models that reject a sampling temperature work from the first request.\n- Object-typed workflow inputs can now declare a nested schema. In the workflow builder, choosing the Object type (or an array of objects) opens a property editor where you define each field with its type, description, allowed enum values, and required flag, including nested objects and arrays up to five levels deep. Declared fields are enforced when a run starts: missing required fields and type mismatches are rejected with precise field paths. Downstream steps get full autocomplete and type resolution for nested references such as `\\{\\{ input.metadata.vendor.name }}`, and the declared shape appears in the scope browser and in generated API documentation. The builder flags expressions that reference undeclared nested fields, the run form and evaluation input editor pre-fill the declared required fields, and the AI expression generator sees the nested shape when suggesting expressions. The headless execution server now enforces declared inputs with the same validator, so its API documentation and runtime behavior agree.\n- Grounded OpenParser extraction results can now carry complete field lineage through the Eigenpal web runtime, preserving source evidence and confidence metadata for downstream review experiences.\n\n### Public API\n\n- Object-typed workflow inputs can now declare a nested schema. In the workflow builder, choosing the Object type (or an array of objects) opens a property editor where you define each field with its type, description, allowed enum values, and required flag, including nested objects and arrays up to five levels deep. Declared fields are enforced when a run starts: missing required fields and type mismatches are rejected with precise field paths. Downstream steps get full autocomplete and type resolution for nested references such as `\\{\\{ input.metadata.vendor.name }}`, and the declared shape appears in the scope browser and in generated API documentation. The builder flags expressions that reference undeclared nested fields, the run form and evaluation input editor pre-fill the declared required fields, and the AI expression generator sees the nested shape when suggesting expressions. The headless execution server now enforces declared inputs with the same validator, so its API documentation and runtime behavior agree.\n- Listing runs with `sinceLastClosed=true` works again. Once a review had been closed, asking for the runs recorded since then returned an error instead of the runs, so review tooling that polls for new work had no way to ask \"what has arrived since I last signed off\". The filter now returns those runs.\n\n### Infrastructure & processing\n\n- Model entries in the deployment configuration can now set `disableTemperature: true`. Workflow steps then never send a sampling temperature to that model, which lets reasoning models (and gateways in front of them) that reject the parameter run without a first-call retry.\n- Object-typed workflow inputs can now declare a nested schema. In the workflow builder, choosing the Object type (or an array of objects) opens a property editor where you define each field with its type, description, allowed enum values, and required flag, including nested objects and arrays up to five levels deep. Declared fields are enforced when a run starts: missing required fields and type mismatches are rejected with precise field paths. Downstream steps get full autocomplete and type resolution for nested references such as `\\{\\{ input.metadata.vendor.name }}`, and the declared shape appears in the scope browser and in generated API documentation. The builder flags expressions that reference undeclared nested fields, the run form and evaluation input editor pre-fill the declared required fields, and the AI expression generator sees the nested shape when suggesting expressions. The headless execution server now enforces declared inputs with the same validator, so its API documentation and runtime behavior agree.\n\n---\n\n## 0.11.3 (2026-08-13)\n\n### Platform (Studio)\n\n- API reference pages for starting and reading runs now show complete examples, including `output` and `execution.status`.\n\n### Public API\n\n- Run API: completed-run examples now include `type`, `execution.status`, `output`, `files`, and `error`, instead of a shared async/completed union that dropped those fields. Default GET and list responses populate `execution.retry.nextRun` when a retry exists, and `expand=execution` keeps `schemaValid` instead of clearing it. Child invoke-workflow runs document `parentExecutionId` and `childExecutions`.\n\n### SDK\n\n- Generated run types now distinguish a completed run from the async accepted envelope (`RunAccepted`), and include `parentExecutionId` for child invoke-workflow runs.\n\n---\n\n## 0.11.2 (2026-08-07)\n\n- Internal improvements.\n\n---\n\n## 0.11.1 (2026-08-07)\n\n### Platform (Studio)\n\n- Document and image processing now runs on updated imaging components that carry upstream security fixes.\n\n### Infrastructure & processing\n\n- Document and image processing now runs on updated imaging components that carry upstream security fixes.\n\n---\n\n## 0.11.0 (2026-08-06)\n\n### Platform (Studio)\n\n- Workflow builder chat now works with reasoning models served through a self-hosted `openai-compatible` gateway, instead of failing with a \"temperature is not supported\" error. On models that only accept their own default temperature setting, replies can vary between attempts on the same question.\n- Canonical API hostnames now consistently expose only supported API and operational routes, returning a clear 404 for unsupported discovery paths.\n- Agent Builder attachments now negotiate multipart or storage-direct transfer, so large PDFs and archives can reach the active sandbox without crossing cloud payload limits. Large downloads use the same secure dual path. Self-hosted operators can set `deployment.multipartMaxBytes` in `eigenpal.config.yaml`; the supplied on-prem deployments keep files on multipart and streamed downloads by default.\n- Canonical API hostnames now serve API routes and discovery endpoints reliably without Studio sign-in handling intercepting requests.\n\n### Public API\n\n- Upload and download reusable files up to 100 MiB through one negotiated API: files within the deployment body limit use multipart, while larger cloud transfers use verified storage-direct uploads and short-lived authenticated download links. Retry-safe temporary run and Builder files expire after 24 hours. Existing multipart routes and `studio.eigenpal.com/api/v1` remain compatible, and new integrations can use `api.eigenpal.com/v1`.\n\n### CLI\n\n- The CLI now uses `api.eigenpal.com` as the default cloud API origin and automatically pre-uploads run files that exceed the multipart request limit. Self-hosted users can keep custom API origins and set `EIGENPAL_MULTIPART_MAX_BYTES` to their proxy limit, or `none` to keep all files on multipart.\n\n### SDK\n\n- The TypeScript and Python SDKs now transparently upload large run inputs without crossing cloud request limits, follow secure storage-direct downloads, and default cloud calls to `api.eigenpal.com`. Set `multipartMaxBytes` / `multipart_max_bytes`, or `EIGENPAL_MULTIPART_MAX_BYTES`, to match a self-hosted proxy; use `null` / `None` to keep every run file on multipart. Explicit `files.upload` files remain reusable until deleted.\n- TypeScript file uploads now follow the server-negotiated multipart endpoint, improving compatibility across hosted and self-managed route layouts.\n\n[View full release notes →](/releases/0.11.0)\n\n---\n\n## 0.10.56 (2026-08-05)\n\n### Platform (Studio)\n\n- Agent Builder training chat now shows Pierre file diffs for edit/write, a Pierre syntax-highlighted file view for Read file, secure request-secret password cards (encrypted into the draft; the agent only sees configured/skipped), and a quiet timeline note when you release an agent version from the dashboard.\n\n### Infrastructure & processing\n\n- Production releases that do not change document processing finish faster by reusing verified OCR provider checks from the last successful release, while still re-checking providers when processing code or infrastructure changes.\n\n---\n\n## 0.10.55 (2026-08-05)\n\n### Platform (Studio)\n\n- Agent Builder now resumes your latest draft, clearly distinguishes unsaved edits from saved changes that are not live, and provides direct Save and Release actions. Releases show the current and next version and let you choose a patch, minor, or major update. Dashboard save and release actions remain available to the builder as private context, so it can accurately answer what is live without guessing. Starting a separate draft is explicit, attachments remain available when beginning a conversation, stopped sandboxes can be restarted without losing the draft, and test runs reliably receive JSON-only inputs.\n\n### Public API\n\n- Agent Builder releases now complete live automation synchronization reliably, so a successful release is immediately available without a separate recovery step.\n\n### Infrastructure & processing\n\n- OCR and LLM model catalogs keep loading when the service adds new optional fields, so Studio and Eigenpal no longer fall back to stale model lists during catalog updates.\n- Cloud document processing providers that are already available stay online during routine production updates when their identity is unchanged. New or changed providers stay unavailable until they pass live verification, so customers keep working providers instead of a full outage window.\n\n---\n\n## 0.10.54 (2026-08-04)\n\n### Platform (Studio)\n\n- Workflow Studio now shows each OpenParser OCR model with its actual provider logo, keeps narrow step configuration panels contained, and places settings, theme, and sidebar controls together in a cleaner navigation footer.\n\n---\n\n## 0.10.53 (2026-08-04)\n\n### Platform (Studio)\n\n- OpenParser Studio now visualizes the `openparser@1` document graph directly, including typed elements, figures, tables, relationships, and element-level extraction citations.\n\n### Public API\n\n- OpenParser parse responses now return the unified `openparser@1` document graph, with plain text, canonical Markdown, pages, typed elements, relations, annotations, assets, geometry, confidence, and provider provenance in one predictable contract.\n\n---\n\n## 0.10.52 (2026-08-04)\n\n### Platform (Studio)\n\n- Agent Builder: improve connection recovery so interrupted sessions resume reliably, stale environments are replaced safely, and approval prompts remain retryable when delivery fails.\n\n---\n\n## 0.10.51 (2026-08-03)\n\n- Internal improvements.\n\n---\n\n## 0.10.50 (2026-08-03)\n\n### Platform (Studio)\n\n- OpenParser Studio: pipeline creation no longer reports a duplicate-name error after a successful save, and edits now save reliably from the browser.\n\n---\n\n## 0.10.49 (2026-08-03)\n\n- Internal improvements.\n\n---\n\n## 0.10.48 (2026-07-30)\n\n### Platform (Studio)\n\n- OpenParser Studio now displays canonical OpenParser JSON with readable indentation, including large document results.\n\n---\n\n## 0.10.47 (2026-07-30)\n\n### Platform (Studio)\n\n- OpenParser Studio now keeps large document results responsive, renders OCR markdown with clearer structure, and provides consistent copy and download actions for Markdown and JSON output. Documentation links now reliably open the generated API reference.\n\n### Infrastructure & processing\n\n- OpenParser jobs now recover more safely when document processing capacity becomes unavailable. Retryable failures pause new dispatches, retry without duplicate charges, and bound large-document processing so one document cannot overwhelm the service.\n\n---\n\n## 0.10.46 (2026-07-30)\n\n### Platform (Studio)\n\n- Team settings: bring back the invitation link. Creating an invitation now shows its link straight away whenever no invitation email was delivered, and administrators can pull up the link again at any time from the invitations table. This unblocks onboarding on deployments without email delivery, where handing over the link is the only way to get someone into the workspace.\n- Run details: field grounding now labels flagged fields by what the extraction actually reports, showing \"3 with low confidence\" instead of \"3 to review\". The count is a confidence signal, not an assigned task.\n\n---\n\n## 0.10.45 (2026-07-29)\n\n### Platform (Studio)\n\n- Parse Document setup now clearly separates OCR from Vision AI, automatically offers available OpenParser OCR models, shows each model's per-page price, displays only the controls that affect the selected method, and surfaces structured layout data with tables, figures, polygons, and bounding boxes for downstream workflow steps.\n\n### CLI\n\n- The workflow authoring reference now covers explicit OCR and Vision AI parse modes, optional figure-caption models, and structured layout output.\n\n### Infrastructure & processing\n\n- Document parsing now defaults to higher-fidelity OCR, preserves canonical structured layout alongside Markdown, and automatically supports available OCR models with provider-specific per-page pricing. Other OCR providers retain their existing rates.\n\n---\n\n## 0.10.44 (2026-07-28)\n\n- Internal improvements.\n\n---\n\n## 0.10.43 (2026-07-27)\n\n- Internal improvements.\n\n---\n\n## 0.10.42 (2026-07-27)\n\n- Internal improvements.\n\n---\n\n## 0.10.41 (2026-07-27)\n\n### Platform (Studio)\n\n- Run traces now show each attempt of an AI request. A request that timed out and was retried used to appear as a single long bar, so a step that spent three timeouts on one page looked identical to one slow call. The raw trace now has one bar per try, labelled with the try number, and the run timeline marks a retried call with how many times it was sent. Retried requests also record how long they waited for a free slot, which distinguishes a slow model from a queue backed up behind its own in-flight requests. For documents parsed page by page, each split retry records the pages it covered and how deep the split went.\n- Runs and Experiments improvements: - An experiment run now shows the same input and output panels as the main run view, including input file downloads, per-step output files, and inline field-level output review. - Scored runs show their eval score in the runs list and in the run header, so an experiment's results can be compared without opening each run. - A run that came from an experiment or a dataset example now links straight to that run in the Evals view. - The Live and Experiments toggles on Runs are remembered between visits. - Field grounding: a wide document no longer overflows the panel. Wide tables and long unbroken text scroll inside the document card.\n\n---\n\n## 0.10.40 (2026-07-25)\n\n### Infrastructure & processing\n\n- Processing reliability on busy deployments: health checks are now served independently of document processing, so the service stays responsive to them even while working through a heavy document. It is no longer restarted mid-run just because it is under load, so long-running document runs complete more reliably.\n- Document processing is more resilient on CPU- and memory-constrained deployments. Page rasterization for vision and OCR now runs in isolation, so a single large or malformed document can no longer stall the processing service or cause it to restart mid-run. Runs on big scanned documents that previously failed partway through with \"the worker stopped before this attempt recorded a terminal result\" now complete reliably.\n\n---\n\n## 0.10.39 (2026-07-25)\n\n### Platform (Studio)\n\n- Label Pages runs: the page preview now shows the classified document in the full viewer instead of small thumbnails. PDFs render page-by-page with zoom and navigation and each page's labels shown beside it; Word and other Office documents open in their own preview next to a per-page label list. Filter by a label to focus on just the pages that carry it.\n\n---\n\n## 0.10.38 (2026-07-24)\n\n### Platform (Studio)\n\n- OpenParser Studio: manage extraction pipelines from a dedicated workspace — create, edit, run, duplicate, and delete saved configurations. Jobs show the pipeline and version that produced each result, and you can save a new pipeline from job settings.\n- OpenParser Studio: clearer progress while documents upload, parse, and extract, with live elapsed timing at each stage. Job history and the playground share a unified header with breadcrumbs, improved document and Markdown output viewers, and a cleaner parse sidebar. Choose light or dark theme and manage your account from the sidebar; model selectors and in-product copy are simpler throughout the workspace.\n- Members provisioned through SSO are now read-only in workspace member settings. Their role and access come from your identity provider's groups, so admins can no longer change or remove them by hand, and each SSO member is clearly labeled. Accounts created with a username and password stay fully manageable by admins.\n\n### Public API\n\n- **Breaking:** OpenParser API resource identifiers now use consistent prefixed ids. Parse, extract, and batch jobs return `opj_…` ids instead of UUIDs. Saved extraction pipelines use `oppl_…` instead of `oep_…`. New parse output writes figure image ids as `opfig_…` instead of `ocrfig_…`. Existing jobs and pipelines are remapped automatically on upgrade when the OCR queue is idle enough for migration preflight. Prefer the new formats in job URLs, `parse_job_id`, `pipeline_id`, and batch child `job_id` fields. During the transition, previously issued UUID job ids, `oep_…` pipeline ids, and UUID list cursors remain readable; responses continue to return the preferred `opj_…` / `oppl_…` forms. Historical parse artifacts and `result_json` may still embed legacy `ocrfig_…` figure paths; `GET /files/\\{id}` accepts both `opfig_…` and `ocrfig_…`.\n\n### Infrastructure & processing\n\n- Hardened AI-powered document processing against prompt-injection attempts hidden inside uploaded documents. Instructions embedded in a document can no longer steer extraction, classification, or vision results away from the requested schema, or coax the model into disclosing its own configuration.\n- AI vision step results no longer include internal document storage references in their metadata. The step's `_vision` provenance now records only which pages were inspected and how many were merged, keeping internal storage paths out of workflow output.\n\n---\n\n## 0.10.37 (2026-07-24)\n\n### Platform (Studio)\n\n- New **Label Pages** step tags each page of a document with zero or more labels (multi-label) using AI, and groups the results into a label-to-pages map. Unlike section splitting — which produces one contiguous range per section — this collects pages of the same kind wherever they appear, so scattered pages (for example every signature or photo page throughout a report) can be gathered in one place. The page-inspection step can now take an explicit list of page indices, so you can route the pages for a given label straight into a visual inspection without hand-listing them.\n\n---\n\n## 0.10.36 (2026-07-23)\n\n### Platform (Studio)\n\n- OpenParser Job History now reports the real end-to-end duration for completed parse and extraction jobs instead of showing a zero or unavailable measurement.\n- Running OpenParser jobs now show the document scanning animation from the playground while results are being prepared.\n- Parse and Extract now use consistent operation icons across OpenParser Studio and documentation.\n\n---\n\n## 0.10.35 (2026-07-23)\n\n### Platform (Studio)\n\n- OpenParser sign-in and account creation now redirect directly to the branded authentication flow without an intermediate confirmation page.\n- OpenParser parse jobs now retain both `raw` and `openparser@1` results. Retrieve either representation with `GET /jobs/\\{id}/result?format=...`, switch formats in the Studio Output tab, and start extraction from a raw-format parse job without running OCR again.\n\n### Public API\n\n- OpenParser parse jobs now retain both `raw` and `openparser@1` results. Retrieve either representation with `GET /jobs/\\{id}/result?format=...`, switch formats in the Studio Output tab, and start extraction from a raw-format parse job without running OCR again.\n- OpenParser now keeps recognized text aligned with its original bounding box by default. Set `ocr_options.merge_layout_blocks` to `true` per parse, extraction, batch item, or saved pipeline when merged recognition is preferred over one-to-one text geometry.\n\n---\n\n## 0.10.34 (2026-07-23)\n\n### Platform (Studio)\n\n- OpenParser sign-in and account creation now keep OpenParser branding throughout the authentication experience.\n- OpenParser Studio: uploads and sample documents now open their Job History detail immediately, so browser back and forward navigation follows each run naturally while extraction and result review stay in one place.\n- Run detail now shows clean, readable file names for inputs and outputs instead of internal storage identifiers, and long file names no longer overlap the preview and download buttons.\n\n### Public API\n\n- OpenParser parsing now preserves every detected layout region—including empty title, header, and figure boxes—so document overlays remain complete. The obsolete `figure_assets` option is removed because figure regions are now always retained.\n- OpenParser can now process documents beyond 25 pages and admit batches of up to 100 documents, while retaining per-page safety limits.\n- OpenParser jobs can now stream their retained source documents with `GET /jobs/\\{id}/source`, making it easier to build durable review experiences around past runs.\n- `POST /suggest-schema` now returns the suggested schema inline as a convenience response instead of creating a job that clients must poll.\n\n### Infrastructure & processing\n\n- Documents uploaded without a declared file type are now recognized from their contents, so PDFs, RTF, and Office files parse reliably instead of failing with an \"unsupported file type\" error. This most often helped uploads coming from automated integrations that omit the file type.\n\n---\n\n## 0.10.33 (2026-07-22)\n\n### Platform (Studio)\n\n- OpenParser Studio: make Playground the home page, add shareable social previews, clarify job status and pipeline empty states, and tidy the OCR model selector copy.\n\n### Public API\n\n- OpenParser suggested extraction model catalog (`GET /models/llm` in suggested mode) now surfaces up to 20 recommended models instead of 12, so more current options appear before you need to search.\n\n---\n\n## 0.10.32 (2026-07-22)\n\n- Internal improvements.\n\n---\n\n## 0.10.31 (2026-07-22)\n\n### Platform (Studio)\n\n- OpenParser Studio production sign-in no longer auto-redirects to Eigenpal when you are signed out. Choose **Continue to sign in** on the local OpenParser page to authenticate with your Eigenpal account; sign-out stays on OpenParser.\n\n---\n\n## 0.10.30 (2026-07-22)\n\n### Platform (Studio)\n\n- OpenParser production sign-in now shares Eigenpal sessions across its dedicated domain and returns users to their original OpenParser destination after authentication.\n- OpenParser Playground: remove the doubled border and rounded corners around the document workspace.\n\n### Infrastructure & processing\n\n- OpenParser: restore schema suggestions, use GPT-5.6 Terra as the default extraction model with GPT-4.1 Mini as a certified fallback, tighten compatibility checks, and provide clearer failure guidance.\n\n---\n\n## 0.10.29 (2026-07-22)\n\n### Platform (Studio)\n\n- OpenParser Studio sign-in and sign-up now load the shared identity UI correctly for signed-out visitors.\n\n---\n\n## 0.10.28 (2026-07-22)\n\n### Platform (Studio)\n\n- API keys: keep usable credentials in the primary list and move revoked or expired credentials into a collapsed history section for faster scanning.\n- OpenParser Studio sign-in and sign-up now show OpenParser product copy and logo while retaining the shared Eigenpal identity.\n\n### Public API\n\n- OpenParser model discovery (`GET /models/llm`) now refreshes the live compatible catalog on the API service instead of falling back to a short emergency list when the catalog credential is missing from that process.\n\n### Infrastructure & processing\n\n- OpenParser document processing now bounds source downloads and retries transient storage failures, preventing stalled jobs from running indefinitely.\n\n---\n\n## 0.10.27 (2026-07-21)\n\n### Platform (Studio)\n\n- New Inspect Pages (Vision) step lets a workflow look at the actual page images of a document and return structured results, for details that only exist in the pixels rather than the text. Use it to check whether a document is signed, rate embedded photos, or spot stamps and logos. It works on PDFs, images, and Office documents, and you can point it at a specific page range (for example the section found by a Split step) so it only reviews the pages that matter. The run trace shows the pages it inspected next to the results.\n- Parsing can now describe the figures on a page as text. Enable \"describe figures\" on a Parse step and the images on each page (property photos, charts, stamps) are detected and captioned inline, so later steps that work on text, like Split, can find image-only pages. This is especially useful for scanned documents, where the photo pages carry almost no text on their own. You can provide custom instructions for how figures should be described.\n- Split step results are now also available keyed by section name. A later step can point at one section directly, for example `\\{\\{ steps.split.output.sections.photos.page_range }}`, instead of filtering the full list. This makes wiring a Split step into a downstream step much simpler.\n- The Inspect Pages (Vision) step now takes a Page From / Page To range instead of a single page-range expression, and both are optional. Leave them blank to inspect the whole document, which is processed in chunks and the results merged automatically. This makes it much easier to point the step at a range (for example the start and end of a Split section) or to scan an entire document without writing a range expression.\n\n---\n\n## 0.10.26 (2026-07-21)\n\n### Platform (Studio)\n\n- Wildcard API keys can use the OpenParser OCR API consistently with every other platform capability. Keys can also be restricted to the dedicated `ocr:full` scope when OCR-only access is preferred.\n- Run traces now include a Grounding panel for extract steps: every extracted value is highlighted in the input text, tinted by confidence, and clicking a field or a highlight jumps between them. Grounded fields also show inline context previews of the exact source passage, both in the panel and in the expanded step row. Split steps gain the same visual page-range panel the segmenter uses, with page thumbnails and per-section preview. The workflow builder reflects that grounding is on by default and lets you pick any configured provider as the grounding model, and capturing expected output from a run no longer snapshots volatile grounding metadata into evals.\n\n### Public API\n\n- Wildcard API keys can use the OpenParser OCR API consistently with every other platform capability. Keys can also be restricted to the dedicated `ocr:full` scope when OCR-only access is preferred.\n- OpenParser: completed extraction jobs can now be retrieved reliably with their attempt and usage metadata.\n\n### CLI\n\n- The step catalog now documents grounded extraction as the default: extract steps ship per-field confidence and source spans under the `_grounding` output key unless `grounded: false` is set.\n\n### Infrastructure & processing\n\n- Data extraction now verifies itself by default. Every extract step traces each extracted field back to its exact location in the source text and attaches a confidence level (high, medium, or low) plus a needs-review flag under a reserved `_grounding` output key, so downstream steps and evaluators can route uncertain values to a human. The verification pass runs on your workspace model (any configured provider), chunks long documents automatically, and falls back to deterministic text matching instead of failing the run when no model is available. Set `grounded: false` on a step to opt out.\n\n---\n\n## 0.10.25 (2026-07-20)\n\n### Platform (Studio)\n\n- Failed single sign-on attempts now return to the sign-in page with a clear, actionable message (for example, when an account's email domain isn't authorized or the account hasn't been granted access) instead of a blank error page.\n\n---\n\n## 0.10.24 (2026-07-15)\n\n### Platform (Studio)\n\n- Agent runs graded by configured evaluators now record a weighted score and pass/fail result, visible in the runs feed and run detail responses. Previously only workflow runs carried an evaluator verdict.\n- Email addresses for a workflow are unique across your whole organization, not just the workflow you are editing. Picking one that is already taken used to fail with a server error. It now tells you which workflow already receives email at that address, or that the workflow you are editing already has it, so you know what to do next. Duplicate folder names and duplicate workspace invites also report clearly instead of failing.\n- `ai.extract` with `grounded: true` no longer succeeds silently without producing grounding. When the grounding pass cannot run (missing OpenAI credentials on the worker, or a grounding model that is not an OpenAI model) the step now fails with an error that names the exact misconfiguration. When the pass runs but produces no field grounding, the output carries an explicit degraded marker instead of nothing. The grounding model picker in the workflow builder now only offers OpenAI models, matching what the grounding pass supports.\n- Agent training: when a training session fails to open, the dashboard now shows what went wrong instead of a generic error. The message names the setup step that failed, and a session blocked by secrets that cannot be decrypted says so and points at the agent settings. A session whose setup fails partway through no longer opens on an incomplete workspace.\n- Self-hosted deployments can now sign users in through corporate single sign-on services that use a redirect-and-token flow instead of OIDC. The new protocol supports mutual TLS for the server-to-server exchange, maps directory groups to EigenPal roles (admin, member, viewer) on every SSO sign-in so the directory stays the source of truth, and links existing password accounts by email so nobody loses access when SSO is turned on. Startup validation rejects unsafe callback origins and provider identifiers, while malformed or oversized identity responses fail closed.\n\n### Public API\n\n- Syncing an automation immediately after a release no longer fails with HTTP 500 while the new release tag propagates. The sync endpoint now reports a retryable status with a clear message when the Git source is momentarily unavailable.\n- Fixed single-experiment results export returning HTTP 400 whenever the `includeTrace` query flag was omitted. `workflow experiment results` and the auto-pull at the end of `workflow experiment watch` now download per-evaluator results in JSON and CSV again.\n- Creating an email trigger address that is already in use now returns 409 with a message naming the workflow in your organization that already uses it, instead of 500.\n\n### CLI\n\n- Agent example runs now report a real evaluator verdict in the CLI: `eigenpal run agents.<name> --example <name> --wait` waits for the verdict, prints the weighted score and pass/fail result, and `--fail-on-mismatch` exits non-zero when the run fails its evaluators, matching the existing workflow behavior. When no evaluators are configured, the CLI says so instead of silently reporting nothing.\n- `agents secrets` is now one command group with `list`, `set`, `unset`, `import`, and `export` subcommands (`agents secret` remains as an alias). The new `agents secrets list` shows the secret names and descriptions declared in the package without ever printing values. Previously `set` and `unset` lived under a separate singular command and `secrets list` failed with an argument error.\n- `agents sync` retries automatically with a short backoff when the server reports the Git source as temporarily unavailable, so the sync right after `agents release` succeeds without manual retries.\n- `agents dataset validate` now checks the same layout the server accepts on `agents dataset push`: example folders under `examples/`, file inputs referenced from `input.json` with `\\{ \"$file\": \"input/...\" }`, and `expected.json` optional. Previously the local validator demanded a different layout that push would then reject, so no dataset could satisfy both. Validation errors now point at the exact rule that failed, including a hint when example folders sit at the dataset root instead of under `examples/`. When the agent package carries an input schema, each example's full input object is validated against it (required fields, unknown keys, and value types, with `\\{ \"$file\": ... }` references accepted for file fields), so inputs that would fail at run start are caught locally. Failure-expected examples (`expected.json` with `$error`) are rejected for agent datasets, since agent runs are evaluated only when they complete.\n\n[View full release notes →](/releases/0.10.24)\n\n---\n\n## 0.10.23 (2026-07-15)\n\n- Internal improvements.\n\n---\n\n## 0.10.22 (2026-07-14)\n\n### Platform (Studio)\n\n- Organization members can now configure, test, and manage outbound webhooks from Studio.\n\n---\n\n## 0.10.21 (2026-07-14)\n\n### Platform (Studio)\n\n- Webhooks: notify external systems when workflow or agent runs are created or change status, with signed delivery, custom headers, retry history, testing, and manual redelivery from Studio.\n- Webhook signing secrets are now easier to copy securely from endpoint setup and details, with clearer event selection defaults.\n- Webhook setup now keeps failed submissions actionable with clear inline error feedback.\n\n### Public API\n\n- Run creation and rerun requests now publish signed lifecycle webhooks only after inputs are ready, so API-triggered automations never emit events for runs that fail during setup.\n\n### Infrastructure & processing\n\n- Improved staging worker deployment reliability when compute capacity is fully allocated.\n\n---\n\n## 0.10.20 (2026-07-12)\n\n### Platform (Studio)\n\n- Workflows: configure automatic retries from Studio at the workflow level or override them for individual HTTP GET/HEAD and Website Reader steps.\n\n### CLI\n\n- The bundled workflow reference now documents durable retry policies, inheritance, supported step types, and retry safety limits.\n\n### Infrastructure & processing\n\n- Workflow runs can recover replay-safe network steps from transient failures without restarting completed work, using bounded backoff and durable retry state. Retry behavior is now step-scoped. Legacy whole-run retry settings no longer restart a failed workflow; configure durable retries on eligible steps or through the workflow retry default instead.\n\n---\n\n## 0.10.19 (2026-07-09)\n\n### Platform (Studio)\n\n- Workflow composition is unified on `action.invoke-workflow`: use `execution: inline` (default) to run a target workflow's steps inside the parent run, or `execution: child` for a separate child run. Reusable blocks (`control.block`, `kind: block`) are removed — migrate to inline invoke with `workflow: <name-or-id>`.\n\n### Public API\n\n- Workflow run endpoints now preserve inline invoke snapshots across initial runs, reruns, and dataset executions so composed workflows remain reproducible after target workflows change.\n\n### CLI\n\n- Workflow YAML reference docs now describe `action.invoke-workflow` as the single composition primitive, including inline and child execution modes.\n\n---\n\n## 0.10.18 (2026-07-08)\n\n### Infrastructure & processing\n\n- On-prem deployments: improve OpenShift compatibility so migration and processing jobs can start reliably under random-user security policies.\n\n---\n\n## 0.10.17 (2026-07-08)\n\n### Platform (Studio)\n\n- Document parser: vision parsing can now render PDF pages at higher resolution and image quality for documents where small text or compression artifacts hurt OCR accuracy.\n\n### Infrastructure & processing\n\n- Document parser: vision parsing can now render PDF pages at higher resolution and image quality for documents where small text or compression artifacts hurt OCR accuracy.\n\n---\n\n## 0.10.16 (2026-07-07)\n\n### Platform (Studio)\n\n- Workflow experiment results now preserve evaluator error details alongside each per-evaluator score, so example grading views and tools can distinguish failed judges from unknown results instead of treating them as passing.\n\n### CLI\n\n- `eigenpal run <workflow> --example <name>` now actually grades the example instead of printing `PASS` whenever the run merely finished. It runs the workflow's current published version on the server's stored dataset example, so configured evaluators run automatically and the CLI surfaces the real weighted score and per-evaluator pass/fail. Workflows with no evaluators fall back to a structural diff of the output against the example's stored expected output. The summary reports two separate signals: execution health (`ok`/`errored`) and accuracy. Add `--fail-on-mismatch` to exit non-zero when a graded example fails. Because the run uses the server-side example, push your latest dataset and evaluators (`eigenpal workflow dataset push`, `eigenpal workflow evaluators push`) before running `--example`.\n\n---\n\n## 0.10.15 (2026-07-06)\n\n### Platform (Studio)\n\n- Experiments: make result review easier in Studio with trace exports, clickable score bars, calmer live refreshes, developer-view defaults, and clearer separation between below-threshold results and run errors.\n\n### Public API\n\n- Experiment export endpoints now support JSON exports with full run trace details for deeper debugging and audit review.\n\n### SDK\n\n- SDK clients now expose the trace-inclusive experiment export option for deeper debugging and audit review.\n\n---\n\n## 0.10.14 (2026-07-06)\n\n### Platform (Studio)\n\n- Evaluations: expected workflow failures now report cleaner scores when a run is supposed to stop early. Studio marks non-applicable quality judges as skipped, so successful rejection examples can receive a full score from the structured failure checks that actually apply.\n\n---\n\n## 0.10.13 (2026-07-06)\n\n- Internal improvements.\n\n---\n\n## 0.10.12 (2026-07-06)\n\n- Internal improvements.\n\n---\n\n## 0.10.11 (2026-07-03)\n\n- Internal improvements.\n\n---\n\n## 0.10.10 (2026-07-03)\n\n- Internal improvements.\n\n---\n\n## 0.10.9 (2026-07-02)\n\n### Platform (Studio)\n\n- Build document-processing workflows that split, route, and verify a mixed batch: - **Document separation** detects the boundaries and types of the documents inside one large scan, so a single upload can be broken into its individual documents and each sent to its own extraction pipeline — even for batches hundreds of pages long. - **Multi-way routing** sends each detected document to the right branch by its type, and stays readable on the canvas even with many branches. - **Grounded extraction** attaches a confidence level and source location to each extracted field, flagging values that aren't found verbatim in the document so reviewers know where to look. - The run view now lets you open the detected documents and preview their pages, and inspect the OCR layout with bounding boxes drawn over the page (with a toggle to the extracted text) — right from the step timeline.\n\n### CLI\n\n- The workflow step catalog now lists the new document-separation and multi-way routing steps, plus the grounded-extraction options, so authoring and validating workflows from the CLI stays in step with what the platform supports.\n\n### Infrastructure & processing\n\n- Self-hosted (on-prem) deployments can now run official EigenPal release images, with access provisioned per customer. Images are published only after a release ships and are signed so you can verify their authenticity before deploying.\n\n---\n\n## 0.10.8 (2026-07-01)\n\n### Platform (Studio)\n\n- Single-file workflow inputs now display correctly after a dataset import. Previously an input that takes one file could show an empty file slot; imported examples now match each file to the workflow's declared input shape.\n- Evaluator scoring now uses a single pass threshold for the whole workflow. The score-by-example chart's cutoff line and every evaluator's pass/fail are measured against that one threshold, so the chart, the per-evaluator results, and the run gate always agree. Experiment scores are shown in two states — passing (green) or below threshold (amber).\n- The LLM judge evaluator now requires evaluation criteria. Without them the model invents its own scoring rubric, so the editor marks the field required, renames it to \"Evaluation criteria\" with a worked example, and blocks saving until it is filled in.\n- Workflow folders can now be reorganized. A new \"Move to folder\" action in each folder's menu lets you move a folder (and everything inside it) into another folder or back to the top level, so you can restructure your workspace without recreating folders. Destinations that would nest a folder inside itself are blocked, with a tooltip explaining why.\n- Nested sub-workflows now display and resolve correctly across runs, the workflow builder, and the CLI. In the Runs list, a run that invokes a sub-workflow which itself invokes another sub-workflow now nests every level under its parent, instead of stopping at the first level. In the workflow builder, an \"Invoke Workflow\" step that targets a workflow built from other invoked workflows now shows the real output field types instead of \"unknown\", so you can reference those fields with autocomplete; referencing one of those fields no longer triggers a false \"field not found\" warning. The same resolution now applies when validating and publishing workflows (including via the CLI), so deeply nested invoke chains validate against their real output fields. All of this resolves through arbitrarily deep chains with a safety limit that prevents looping on workflows that invoke each other.\n- Runs: add a simplified review mode with developer-view toggle, review status badges, and inline output review for JSON fields and generated files.\n- New debugging and control primitives for runs. You can now cancel an in-progress run through the API, experiment results report when a batch has fully finished, steps skipped by a condition or an empty loop are reported as skipped rather than succeeded, and each step records the exact configuration it ran with so failures are easier to diagnose.\n- Run detail responses are leaner and more explicit: you choose exactly which sections to expand, and the public run schema now exposes the workflow version, cancellation-request time, and trace, retry, and email-trigger details.\n\n### CLI\n\n- `eigenpal workflow validate` now tells you when a workflow contains Invoke Workflow steps that local validation cannot fully check. Because those steps reference other workflows that live on the server, the command prints a short note pointing you to `validate --online` (or push) to verify the invoke targets, input types, and cycles. The skill guidance now shows the `--online` step in the validate-then-push flow as well.\n- The evaluator configuration reference now reflects the single workflow-level pass threshold. A per-evaluator `passThreshold` is documented as legacy — pass/fail is decided by the one workflow-level threshold — and that workflow-level threshold no longer shows a fixed default, since it is derived when omitted.\n\n### SDK\n\n- The Python SDK now requires Python 3.10 or newer. Python 3.9 has reached the end of its support window, and dropping it keeps the SDK on current, actively maintained dependencies. If you are still on Python 3.9, upgrade to 3.10+ to continue receiving SDK updates.\n\n[View full release notes →](/releases/0.10.8)\n\n---\n\n## 0.10.7 (2026-06-30)\n\n### Platform (Studio)\n\n- Faster dashboard navigation. Lists keep their contents on screen while filters and folders refresh instead of flashing a loading state, document and image previews are cached so they no longer re-download on every navigation, and heavy viewers (PDF, charts, rich text) load on demand for a quicker first paint.\n- Workflows that invoke another workflow now pass file inputs through to the sub-workflow, so file-backed sub-workflow runs no longer fail before they start.\n\n### CLI\n\n- `eigenpal --help` now groups `run`, `rerun`, and `runs` under a dedicated \"Run\" section instead of the catch-all \"Other\" bucket, so starting a run and reading its output is easy to find.\n\n---\n\n## 0.10.6 (2026-06-30)\n\nThis release contains maintenance and reliability improvements with no customer-facing changes.\n\n---\n\n## 0.10.5 (2026-06-29)\n\nThis release makes EigenPal's public changelog a reliable product record. Each release now has a dedicated docs page that explains what changed for Studio, the public API, the CLI, SDKs, and document processing in one place.\n\nThat means teams can follow platform improvements without piecing together GitHub activity or package-specific notes. The changelog is organized around customer-facing product areas, so it is easier to see which changes affect app users, API integrations, developer tools, and processing behavior.\n\n### Platform (Studio)\n\n- Invoke Workflow steps are now typed end to end. Pick a target workflow and map each input it declares to a value, then reference the workflow's output fields and any files it generates from downstream steps with autocomplete. Inputs and outputs are checked when you save and when the step runs, so mismatches surface early instead of failing mid-run.\n- Runs that invoke another workflow now appear nested under their parent run in both the runs feed and the run timeline, and link straight to the sub-workflow's own run and trace.\n- You can view a workflow's inferred output schema (the fields and files it returns, typed from its own steps) without running it.\n- File inputs are preserved consistently across runs, reruns, reviewed examples, and dataset experiments, so reused runs and examples no longer lose or mispoint their files.\n- Creating new organizations and other Git-backed source operations stay reliable even during Git provider outages.\n\n### Public API\n\n- Run file inputs now use a consistent artifact model. Callers provide reusable files with `$fileId` or inline files with `$inline`, and EigenPal materializes them into run-owned artifacts before execution, reruns, promotion, and dataset workflows.\n- A new endpoint returns a workflow's inferred output schema without running it, as JSON Schema or as a TypeScript or Python type.\n\n### CLI\n\n- `eigenpal workflow schema <workflow>` prints a workflow's inferred output schema as JSON Schema, or as a TypeScript or Python type with `--format`.\n\n[View full release notes →](/releases/0.10.5)\n\n---\n\n## 0.10.4 (2026-06-25)\n\n### Platform (Studio)\n\n- Unify file list review surfaces\n\n---\n\n## 0.10.3 (2026-06-24)\n\n### SDK\n\n#### Patch Changes\n\n- e48cb81: Remove `fileVerdict` from run review correction types; file correction intent is encoded via `correctedArtifactPath`.\n\n---\n\n## 0.10.2 (2026-06-24)\n\n### CLI\n\n#### Patch Changes\n\n- ca9ae4f: Workflow inputs can declare an external file `source` (single-tenant on-prem). A `type: file` input with `source: <name>` (e.g. `gpfs`) is provided as a plain string id at run time; the worker resolves it to a file artifact via the configured source before the workflow executes.\n\n---\n\n## 0.10.1 (2026-06-23)\n\n### Public API\n\n- Polish reviews pages and group API reference nav\n\n### Infrastructure & processing\n\n- Client-shareable deployment architecture PDFs\n\n---\n\n## 0.10.0 (2026-06-23)\n\n### Platform (Studio)\n\n- Add simple runs review mode\n\n### Infrastructure & processing\n\n- Autogenerate chart architecture diagram from helm template\n- AWS/GCP/Azure Terraform examples with architecture diagrams\n- Helm chart + local kind for single-tenant on-prem\n\n---\n\n## 0.9.0 (2026-06-22)\n\n### CLI\n\n#### Minor Changes\n\n- c35d5d4: Clean up the public runs and evaluation API surface: move workflow eval operations onto automations, expose run scores/artifacts through the unified runs API, and update CLI/SDK helpers to use the canonical routes.\n\n Breaking: `runs.evalResults` and `GET /api/v1/runs/\\{id}/eval-results` are replaced by `runs.scores` and `GET /api/v1/runs/\\{id}/scores`.\n\n#### Patch Changes\n\n- c35d5d4: Clarify `--max-wait` help text so timeout exit behavior reads naturally.\n\n### SDK\n\n#### Minor Changes\n\n- c35d5d4: Clean up the public runs and evaluation API surface: move workflow eval operations onto automations, expose run scores/artifacts through the unified runs API, and update CLI/SDK helpers to use the canonical routes.\n\n Breaking: `runs.evalResults` and `GET /api/v1/runs/\\{id}/eval-results` are replaced by `runs.scores` and `GET /api/v1/runs/\\{id}/scores`.\n\n[View full release notes →](/releases/0.9.0)\n\n---\n\n## 0.8.0 (2026-06-18)\n\n### CLI\n\n#### Minor Changes\n\n- d620a00: Expose the clean public automation API around automations, runs, files/artifacts, datasets, evaluators, experiments, evaluation results, and run promotion. This intentionally breaks the pre-v1 SDK/CLI surface that exposed workflow- and agent-specific top-level resources outside the explicit legacy compatibility routes.\n\n > [!WARNING]\n > **Breaking change (pre-v1).** The workflow- and agent-specific top-level\n > resources have been removed in favor of a single automation-centric surface.\n > These packages are still 0.x, so this breaking redesign ships as a `minor`\n > bump. Update your integration before upgrading.\n\n #### Removed\n - Top-level workflow resources, e.g. `client.workflows.*`, including\n `client.workflows.executions.runAndWait(...)` and the rest of the\n `client.workflows.executions.*` tree.\n - Top-level agent resources, e.g. `client.agents.*` (runs, reviews, traces,\n files, expected artifacts).\n - Top-level source/git resources, e.g. `client.sources.*`.\n\n[View full release notes →](/releases/0.8.0)\n\n---\n\n## 0.7.2 (2026-06-13)\n\n### SDK\n\n#### Minor Changes\n\n- c5a709c: Public workflow list/detail responses include `apiEnabled` and `triggers`; run definition docs note workflow-only scope.\n\n---\n\n## 0.7.1 (2026-06-13)\n\n### SDK\n\n#### Patch Changes\n\n- 9db3f01: Expose `apiEnabled` and `triggers` on `AgentSummary` for runtime trigger projection.\n\n---\n\n## 0.7.0 (2026-06-12)\n\n### CLI\n\n#### Minor Changes\n\n- 963fd6c: Unify run detail on `GET /api/v1/runs/\\{id}`: return the canonical Run object directly (no `\\{ run: ... }` envelope) and merge expanded fields in-place via documented `expand` tokens. Remove the session-only `expand=internal` dashboard escape hatch; SDKs and CLI now use explicit expand lists.\n\n#### Patch Changes\n\n- 963fd6c: Update runs commands for the grouped run API shape and four expand tokens.\n\n### SDK\n\n#### Minor Changes\n\n- 963fd6c: Unify run detail on `GET /api/v1/runs/\\{id}`: return the canonical Run object directly (no `\\{ run: ... }` envelope) and merge expanded fields in-place via documented `expand` tokens. Remove the session-only `expand=internal` dashboard escape hatch; SDKs and CLI now use explicit expand lists.\n\n#### Patch Changes\n\n- 12c00d8: Docs: clearer wording in the file-input and TypeScript-runtime sections. No API or behavior changes.\n\n---\n\n## 0.6.17 (2026-06-12)\n\n### SDK\n\n#### Patch Changes\n\n- 716c3cf: Update the hosted default API base URL and API-key guidance from `app.eigenpal.com` to `studio.eigenpal.com`.\n\n---\n\n## 0.6.16 (2026-06-12)\n\n### CLI\n\n#### Patch Changes\n\n- 75dd64c: Internal refactor: move the skill tool registry into its own module so other tooling can import it. No change to any command, flag, argument, or output.\n\n---\n\n## 0.6.15 (2026-06-10)\n\n### CLI\n\n#### Minor Changes\n\n- ca87265: Unify workflow and agent run starts behind the canonical `/api/v1/run/\\{target}` endpoint, root `eigenpal run` / `eigenpal rerun` commands, and root SDK `client.run()` / `client.rerun()` methods.\n\n The old nested CLI commands and SDK resource methods for starting workflow or agent runs have been removed.\n\n#### Patch Changes\n\n- 5ea0bb5: `workflow dataset pull` gains `--example-id <id>` (repeatable) to export a single example or a selected subset of a dataset, instead of always pulling the whole dataset. The subset ZIP uses the same archive layout as a full pull, so it re-imports into any dataset via `dataset push --mode append`. Requesting an example id that does not exist for the workflow exits non-zero.\n\n### SDK\n\n#### Minor Changes\n\n- ca87265: Unify workflow and agent run starts behind the canonical `/api/v1/run/\\{target}` endpoint, root `eigenpal run` / `eigenpal rerun` commands, and root SDK `client.run()` / `client.rerun()` methods.\n\n The old nested CLI commands and SDK resource methods for starting workflow or agent runs have been removed.\n\n---\n\n## 0.6.10 (2026-06-08)\n\n### CLI\n\n#### Patch Changes\n\n- 99ca1b4: Unify agent and workflow run commands and SDK helpers on the shared `/api/v1/runs` API, including the public `client.runs` facade and regenerated SDK reference docs.\n\n### SDK\n\n#### Patch Changes\n\n- 99ca1b4: Unify agent and workflow run commands and SDK helpers on the shared `/api/v1/runs` API, including the public `client.runs` facade and regenerated SDK reference docs.\n\n---\n\n## 0.6.9 (2026-06-04)\n\n### SDK\n\n#### Minor Changes\n\n- 5e7c051: Add `observability` (phase timeline + structured failure) to workflow execution and agent run status API responses.\n\n---\n\n## 0.6.6 (2026-06-03)\n\n### CLI\n\n#### Patch Changes\n\n- 245f347: Clean up the agent CLI surface by removing obsolete compatibility commands, moving reruns to `agents rerun`, and replacing run pulls with canonical artifact fetching.\n- 5ab00ee: Tighten permissions on `~/.config/eigenpal/credentials.json` and its parent directory to owner-only (0600 / 0700). Previously the file was written 0600 but the parent directory inherited the user's umask (typically 0755), allowing other local users to list the directory and stat the file. The CLI now also re-tightens permissions on every write, so installs created with older versions are corrected on the next `auth login` / `auth use`.\n\n---\n\n## 0.6.4 (2026-06-03)\n\n### CLI\n\n#### Patch Changes\n\n- c3486a5: Fix agent git cutover QA findings: dashboard runs now navigate to the created run, inbound email aliases preserve mixed-case organization qualifiers, sandbox source materializes under `/workspace/agent`, run artifact downloads use direct paths like `/files/eigenpal.lock`, wait-for-completion responses include source provenance, and CLI wait/watch commands exit nonzero for failed or cancelled runs.\n\n### SDK\n\n#### Patch Changes\n\n- c3486a5: Fix agent git cutover QA findings: dashboard runs now navigate to the created run, inbound email aliases preserve mixed-case organization qualifiers, sandbox source materializes under `/workspace/agent`, run artifact downloads use direct paths like `/files/eigenpal.lock`, wait-for-completion responses include source provenance, and CLI wait/watch commands exit nonzero for failed or cancelled runs.\n\n---\n\n## 0.6.2 (2026-06-02)\n\n### CLI\n\n#### Patch Changes\n\n- d2202b6: List all agent runs for unqualified targets, and reserve source-ref filtering for explicit `@ref` targets.\n\n---\n\n## 0.6.0 (2026-06-02)\n\n### CLI\n\n#### Minor Changes\n\n- 2aeeaf2: Add native source `git save` and branch-aware `git release` flows for Git-backed builder workspaces.\n\n#### Patch Changes\n\n- 2aeeaf2: Complete the agent Git cutover with pluralized agent CLI commands, source-ref run support, lockfile metadata, and regenerated SDK/docs surfaces.\n- 2aeeaf2: Expose Git-backed agent source refs and public source automation APIs consistently across CLI docs and SDK facades.\n\n### SDK\n\n#### Patch Changes\n\n- 2aeeaf2: Complete the agent Git cutover with pluralized agent CLI commands, source-ref run support, lockfile metadata, and regenerated SDK/docs surfaces.\n- 2aeeaf2: Expose Git-backed agent source refs and public source automation APIs consistently across CLI docs and SDK facades.\n\n---\n\n## 0.5.12 (2026-06-02)\n\n### CLI\n\n#### Minor Changes\n\n- d3b0768: Add `control.fail` and `ai.classify` step types, plus the `expected/error.json` dataset assertion format.\n - `control.fail` terminates a workflow with a typed status code + message envelope. Optional `condition` template gates the fail. Compose with `ai.classify` to reject documents that match an undesired label.\n - `ai.classify` runs single-label closed-set classification through the existing extract LLM pipeline. Output is `\\{ label, confidence, reason }`, where `label` is constrained to the configured names.\n - `examples/<name>/expected/error.json` (mutually exclusive with `output.json`) lets eval examples assert that a workflow should fail with a specific `\\{ code, messageContains, step }`. The exact-diff evaluator scores 1 on match, 0 otherwise.\n - `eigenpal workflow validate` now catches `expected/error.json` shape issues and per-step config violations (e.g. `ai.classify` with fewer than two labels) at push time instead of deferring to runtime.\n\n Skill reference docs (`step-types.md`, `dataset-format.md`, `evaluators.md`, `workflow-yaml.md`) updated.\n\n#### Patch Changes\n\n- 99cad97: Skill reference (auto-generated from `WorkflowDefinitionSchema`) now documents the new `values` row for enum inputs, alongside the existing `items` row used by array inputs. Output-only doc refresh — no CLI behavior change.\n- d70f697: Document the `control.parallel`, `control.parallel_map`, `control.foreach`, `control.if`, and `control.block` step types in the bundled skill reference. The auto-generated catalog cannot render nested step shapes, so the YAML form, output access paths (e.g. `steps.<parallel>.output.<branch>.<inner>.<field>`), and scoping rules now live as hand-written prose. Also documents the multi-step iteration hazard: only the LAST inner step's output is keyed into each `items[i]` for `parallel_map` / `foreach` — end the body with a `transform.script` if you need to preserve intermediate fields.\n- ed2cde6: Fix `eigenpal workflow pull` writing a 0-byte file. The v1 single-workflow endpoint now returns the current YAML at the top level (`yamlContent`) via a new `WorkflowDetail` response shape, and the CLI throws a clear error instead of silently writing empty output when no version has been published. Also fixes adjacent regressions caused by the same picker: `workflow list` now shows the workflow name and version columns (previously always `-`), `workflow push --bump` reads the server's current version correctly, the \"did you mean?\" suggestion on a slug typo lists candidates again, and `workflow execution list` shows a duration column computed from the run start/end timestamps.\n\n---\n\n## 0.5.11 (2026-05-26)\n\n### CLI\n\n#### Patch Changes\n\n- 2958f4d: Fix Git-backed source smoke-test polish: hidden `git show` and `git sync` now accept bare agent slugs, and workflow scaffolds use the current `triggerMethods` YAML shape.\n\n---\n\n## 0.5.10 (2026-05-26)\n\n### CLI\n\n#### Patch Changes\n\n- aee7e0c: Add hidden Git source commands for editing trigger policy and encrypted secrets.\n\n---\n\n## 0.5.9 (2026-05-26)\n\n### CLI\n\n#### Major Changes\n\n- fadb59b: CLI command tree reorganized around the data model. Top-level surface is now\n `workflow`, `agent` (reserved), and `skill`; everything that operates on a\n workflow lives under `workflow`, grouped by sub-entity.\n\n Renames (clean break — no aliases):\n - `eigenpal exec <slug>` → `eigenpal workflow execution run <slug>`\n - `eigenpal clear` → `eigenpal workflow clear`\n - `eigenpal install-skill` → `eigenpal skill` (same interactive multiselect UX)\n - `eigenpal execution \\{get,list,watch,compare}` → `eigenpal workflow execution \\{…}`\n - `eigenpal workflow set-definition` → `eigenpal workflow push`\n - `eigenpal workflow get-definition` → `eigenpal workflow pull`\n - `eigenpal workflow set-evaluators` / `get-evaluators` → `workflow evaluators \\{push,pull}`\n - `eigenpal workflow set-dataset` / `get-dataset` / `list-examples` → `workflow dataset \\{push,pull,list}`\n - `eigenpal workflow run-experiment` / `get-experiment-status` / `get-experiment-results` /\n `get-eval-results` / `list-executions` → `workflow experiment \\{run,status,results,eval-results,list}`\n - `eigenpal workflow list-versions` / `restore-version` → `workflow version \\{list,restore}`\n\n[View full release notes →](/releases/0.5.9)\n\n{/* GENERATED:CHANGELOG_INDEX END */}\n"
|
|
23
|
+
"content": "---\ntitle: Changelog\ndescription: Production release history for the EigenPal platform.\n---\n\nEigenPal ships on a unified platform version. Each production release updates Studio, the public API, infrastructure, and (when applicable) the CLI and SDKs together.\n\n{/* GENERATED:CHANGELOG_INDEX START */}\n\n## 0.19.0 (2026-09-21)\n\n### Platform (Studio)\n\n- Agent builder sessions can now be given storage credentials limited to their own session folder instead of the whole storage account. Set a Cloudflare account id and API token, or an AWS role, and each training sandbox receives a short-lived key that can only read and write its own files. Deployments that do not configure this keep working exactly as before and log a warning, and a new setting makes the platform refuse to start a sandbox unless scoped credentials are available.\n- Studio security hardening. Parsed document text shown in run traces and grounding views is now sanitized before rendering, so a crafted document can no longer inject active content into the page. Observability settings now require administrator access to view, and the values of any authorization headers you configure are masked once saved, so they are never sent back to the browser. Saving a renamed header now asks you to retype its value instead of quietly clearing the configuration. Outbound observability destinations are validated to reject internal and unencrypted endpoints on hosted plans. Editor assets are now served from Eigenpal's own domain instead of a public content network. Self-hosted deployments that use password sign-in no longer accept open self-registration. Creating the first account still works, and anyone holding an invitation can still create their account, but an uninvited visitor can no longer join an existing workspace. Set `EIGENPAL_ALLOW_OPEN_SIGNUP=true` to restore the previous behavior. Agent training and email trigger surfaces are now scoped more tightly. Email alias changes are accepted only through the agent that owns the alias, so editing or revoking one from a different agent's page now reports not found. Opening a training session that belongs to a different workspace or a different agent is refused instead of returning a server error. Each agent workspace now runs with credentials valid only for its own session, so a workspace can no longer act on any session but its own. Sign-in and account safety. Changing your password now signs out your other sessions, so a password change ends access from a device you no longer control. Accepting an invitation now requires the invitation link itself rather than just the invited address, so knowing that someone was invited is no longer enough to create an account in their name. Requests that arrive from another site are refused, which closes cross-site request forgery against pages you are signed in to. Set `EIGENPAL_TRUSTED_ORIGINS` if you serve Eigenpal on more than one hostname.\n\n### Public API\n\n- Agent endpoints that accept an agent id now resolve it strictly within the calling organization. Requesting an id belonging to another organization returns not found instead of reading or modifying it. Run and step endpoints apply the same organization check to execution ids. Uploads that omit a declared length are now measured while they stream and return 413 once they exceed the request limit, rather than being read into memory first. Operator source tokens are accepted only on the source and authentication check endpoints they are documented for. Webhook endpoints are now checked when you save them, not only when a delivery is attempted. A destination the platform cannot deliver to is rejected with a validation error at creation and update time, instead of being accepted and then failing silently on every send. Self-hosted deployments that deliver to their own internal network are unaffected. Every endpoint that starts a new run now honours the automation's trigger settings, the same way `POST /api/v1/runs` already did. That covers rerunning a run, starting an experiment, and running a single dataset example, including their streaming variants. An API key calling one of these on an automation whose API trigger is turned off now receives `403 api_trigger_disabled`, and a dashboard session calling one on an API-only automation receives `403 manual_trigger_disabled`. If an existing integration starts returning 403, turn the API trigger back on for that automation.\n- Self-hosted Prometheus scrapes now require the dedicated scrape token in every deployment. An organization API key is no longer enough to read the cluster snapshot. A deployment without `EIGENPAL_METRICS_SCRAPE_TOKEN` returns 404 on the metrics route, so set the token before upgrading if you scrape today with an API key. The headless workflow server no longer serves a network address without an API token. If you set `HOST` yourself, the server now exits at startup unless you also set `HEADLESS_API_TOKEN` or `HEADLESS_ALLOW_UNAUTHENTICATED_BIND=1`. If you never set `HOST`, the bind address changes from `0.0.0.0` to `127.0.0.1`, which inside a container means a published port stops accepting connections while the container still reports healthy. Either set `HEADLESS_API_TOKEN` (16 characters or more) together with `HOST=0.0.0.0`, or set `HEADLESS_ALLOW_UNAUTHENTICATED_BIND=1` to keep the previous behaviour. Members can no longer pull decrypted agent environment secrets. Use an owner or admin key, or decrypt a secret for a specific run.\n\n### Infrastructure & processing\n\n- Self-hosted Prometheus scrapes now require the dedicated scrape token in every deployment. An organization API key is no longer enough to read the cluster snapshot. A deployment without `EIGENPAL_METRICS_SCRAPE_TOKEN` returns 404 on the metrics route, so set the token before upgrading if you scrape today with an API key. The headless workflow server no longer serves a network address without an API token. If you set `HOST` yourself, the server now exits at startup unless you also set `HEADLESS_API_TOKEN` or `HEADLESS_ALLOW_UNAUTHENTICATED_BIND=1`. If you never set `HOST`, the bind address changes from `0.0.0.0` to `127.0.0.1`, which inside a container means a published port stops accepting connections while the container still reports healthy. Either set `HEADLESS_API_TOKEN` (16 characters or more) together with `HOST=0.0.0.0`, or set `HEADLESS_ALLOW_UNAUTHENTICATED_BIND=1` to keep the previous behaviour. Members can no longer pull decrypted agent environment secrets. Use an owner or admin key, or decrypt a secret for a specific run.\n- Document and archive processing is more resilient to malformed or hostile input. Dataset archive imports now apply a limit on entry count and hold far less memory while expanding, so a small archive can no longer exhaust a request. Office documents are checked against their real expanded size rather than the size recorded inside the file, which blocks highly compressed payloads that previously passed inspection. PDF page rendering moved to a newer engine that removes a document driven code execution path. Script transform steps have a shorter maximum run time that is now capped relative to worker health checks, so a long running script cannot stall other work. With stock settings the ceiling drops from 30 seconds to 15 seconds; self-hosted deployments that need more can raise `HEALTH_LIVENESS_STALE_MS`, since the ceiling is half that value. Workflow processing is more resilient to hostile input. Template rendering now has time and memory ceilings, so a crafted template can no longer stall a worker and delay other runs. Deep merges in the combine transform can no longer be steered into altering shared state. External file sources reject addresses that are not plain HTTP endpoints.\n\n---\n\n## 0.18.0 (2026-09-21)\n\n### Platform (Studio)\n\n- Workflows can crop normalized regions from PDFs and images into JPEG artifacts with the new `transform.crop-regions` step — pair it with parse output and script steps to build figure or layout evidence pipelines without a figure-specific step type. Turn on optional **`cache: true`** on **`ai.parse`** to reuse prior parse output for the same file bytes and parse settings across runs (stored in tenant blob storage; no expiry yet).\n\n---\n\n## 0.17.5 (2026-09-21)\n\n### Platform (Studio)\n\n- Summary Agent: spreadsheet uploads now use a hardened parser that prevents crafted workbook content from causing excessive processing or modifying object prototypes.\n\n### CLI\n\n- CLI skill reference documents the new `ai.figure-crops` workflow step for cropping and captioning figures from parsed PDFs.\n\n### SDK\n\n- Python SDK: installs now require `anyio` 4.14.2 or newer. Earlier versions encoded host names with IDNA 2003 when opening a TLS connection, so a crafted host name could be matched against the wrong certificate. The TypeScript SDK is unaffected.\n\n---\n\n## 0.17.4 (2026-09-18)\n\n### Platform (Studio)\n\n- Strengthen organization access controls so role permissions, API-key scopes, and member removal apply consistently across Studio and the API. Webhook configuration and signing secrets are restricted to organization owners and admins. Service credentials cannot exceed member permissions, while members retain file and review editing through the API.\n- Organization owners can invite new users directly as owners. Owner invitations can only be created and managed by existing owners.\n\n### Public API\n\n- Strengthen organization access controls so role permissions, API-key scopes, and member removal apply consistently across Studio and the API. Webhook configuration and signing secrets are restricted to organization owners and admins. Service credentials cannot exceed member permissions, while members retain file and review editing through the API.\n\n### Infrastructure & processing\n\n- Cloud workflow HTTP and website-reader actions now restrict requests to public HTTP/S destinations, verify TLS for HTTPS requests, and reject HTTPS-to-HTTP redirects. Responses are limited to 20 MiB and requests to 120 seconds. Single-tenant installations retain private-network integration support.\n\n---\n\n## 0.17.3 (2026-09-18)\n\n### Public API\n\n- Large PDF parses are now more reliable under parallel load. The OCR service paces concurrent pages per document so multi-page jobs no longer exhaust inference memory and retry, which previously stalled big documents behind repeated restarts.\n\n---\n\n## 0.17.2 (2026-09-17)\n\n### Platform (Studio)\n\n- Settings gains a Data retention section for workspace admins: see how much file storage past and present runs consume, choose how long eval and other runs are kept, preview what falls past the horizon, and delete those runs in capped passes until clear. Runs that are still active are always kept. Enforcement offers two modes: purge input files only (run history and outputs stay, freeing the bulky source documents while keeping the audit trail) or delete full runs. Storage usage splits run inputs from outputs so admins can see what input-only cleanup would free.\n\n### Public API\n\n- Rerunning a run whose input files were removed by data retention now returns a clear `409 inputs-purged` error instead of failing while copying the missing files. Run history and outputs remain available.\n\n---\n\n## 0.17.1 (2026-09-15)\n\n- Internal improvements.\n\n---\n\n## 0.17.0 (2026-09-15)\n\n### Platform (Studio)\n\n- Studio can connect to a live agent inference session even when that session runs in a local sandbox instead of the hosted cloud environment.\n\n### Infrastructure & processing\n\n- Agent inference can now run in a local container sandbox, so on-prem and air-gapped deployments no longer need a cloud sandbox provider.\n- Agent runs can now execute in customer-hosted sandbox environments instead of only the Eigenpal cloud, and local Docker sandboxes connect reliably on plain localhost setups.\n\n---\n\n## 0.16.13 (2026-09-14)\n\n- Internal improvements.\n\n---\n\n## 0.16.12 (2026-09-14)\n\n### Platform (Studio)\n\n- Studio can import large dataset ZIP archives without taking the app down, including archives whose paths use Windows-style backslashes. Opening a long-running workflow no longer pulls every step’s full input and output at once. Run pages stay available, still show the steps timeline when phase data is missing, and keep the last loaded view if a refresh fails — with a retry control instead of a blank error page.\n\n### Public API\n\n- `GET /api/v1/runs/\\{id}?expand=execution` and `GET /api/v1/runs/\\{id}/steps` now return slim step rows (identity, status, timing, and a capped error excerpt) instead of inlining every step’s input and output. Inspect one step’s full payloads with the new `GET /api/v1/runs/\\{id}/steps/\\{stepExecutionId}` endpoint.\n\n### CLI\n\n- `eigenpal runs get` lists slim step rows by default. Pass `--include input,output` or `--step` to fetch full payloads for the steps you care about.\n\n### SDK\n\n- The TypeScript and Python SDKs now include `GET /api/v1/runs/\\{id}/steps/\\{stepExecutionId}` so you can load one step’s full input and output after listing slim run steps.\n\n---\n\n## 0.16.11 (2026-09-11)\n\n### Platform (Studio)\n\n- Studio: organization switching and other labeled menus now open reliably without a client-side error.\n\n---\n\n## 0.16.10 (2026-09-10)\n\n### Infrastructure & processing\n\n- OCR processing now distinguishes temporary host availability issues from true processing timeouts, keeps redundant production capacity warm, and pages only when unfinished work is affected. Short self-healing interruptions remain visible for diagnosis without generating misleading recovery alerts.\n\n---\n\n## 0.16.9 (2026-09-10)\n\n### Platform (Studio)\n\n- Harden web image handling, document image processing, and email delivery against crafted inputs that could cause remote code execution, excessive resource use, or recipient-validation bypasses.\n\n### Infrastructure & processing\n\n- Harden web image handling, document image processing, and email delivery against crafted inputs that could cause remote code execution, excessive resource use, or recipient-validation bypasses.\n\n---\n\n## 0.16.8 (2026-09-10)\n\n### Platform (Studio)\n\n- Files are easier to recognize throughout Studio with refreshed Office logos, dedicated PDF, archive, text, JSON, and code treatments, plus image thumbnails. File inputs now use a clearer rounded drag-and-drop experience with consistent selected-file previews.\n\n---\n\n## 0.16.7 (2026-09-10)\n\n### Platform (Studio)\n\n- File uploads now infer supported document media types from the filename when clients provide an empty or generic content type, allowing original Outlook messages and macro-enabled workbooks to upload without conversion.\n\n### Public API\n\n- File uploads now infer supported document media types from the filename when clients provide an empty or generic content type, allowing original Outlook messages and macro-enabled workbooks to upload without conversion.\n\n### CLI\n\n- The CLI now uploads macro-enabled Excel workbooks with the correct media type, so original `.xlsm` workflow inputs work without manual conversion.\n\n### SDK\n\n- TypeScript SDK file helpers now infer the correct media types for `.xlsm` workbooks and `.msg` Outlook messages.\n\n### Infrastructure & processing\n\n- Document parsing now reads Excel, legacy Excel, and macro-enabled workbooks directly, preserving every non-empty sheet and its cell evidence without executing formulas or macros. Large single-page extractions are also split into bounded AI context windows instead of failing at the model limit.\n\n---\n\n## 0.16.6 (2026-09-09)\n\n### Infrastructure & processing\n\n- Template processing now enforces memory limits when joining large collections, preventing crafted templates from exhausting worker memory.\n\n---\n\n## 0.16.5 (2026-09-08)\n\n### Platform (Studio)\n\n- Human review is now available in production, including workflow authoring and review queues.\n\n### Public API\n\n- Human review API endpoints are now available in every production organization for listing tasks, confirming fields, and completing reviews.\n\n### Infrastructure & processing\n\n- Workflow and agent runs can now request human review in every production organization.\n\n---\n\n## 0.16.4 (2026-09-08)\n\n### Platform (Studio)\n\n- Studio adds List Archive and Extract Archive File steps for ZIP workflow inputs. Enumerate archive contents, wire `\\{\\{ item.path }}` through foreach, and pass extracted files straight into Parse, Vision, or Search Files without leaving the builder.\n- Studio now finishes multi-GB file uploads with resumable multipart transfers, retrying individual parts without restarting the whole file.\n- Studio workflows now include Search Files: ask a question about a ZIP archive and get a cited answer plus the files the step inspected. Scope the search to a folder inside the archive and use the same model and reasoning controls as other AI steps.\n\n### Public API\n\n- Upload ZIP archives as reusable workflow inputs through the Files API. Archives can be much larger than single documents, so teams can pass bundled evidence, exports, or multi-file drops into the same runs that already accept PDFs and Office files.\n- File uploads can now negotiate a resumable multipart session for multi-GB objects, with on-demand part URLs, authoritative resume listing, and safe completion that never overwrites a finished file.\n- Starting a run from a reusable file id now keeps a private run-owned reference to the original bytes instead of duplicating large uploads. Deleting the reusable file leaves past runs readable until those runs themselves are deleted.\n\n### CLI\n\n- Dataset import and export now keep a single `/api/v1` prefix when the CLI base URL already includes `/api/v1`, so those commands reach the dataset API instead of a doubled `/v1/v1` path.\n- The CLI now uploads large run files with resumable, storage-direct transfers, so multi-GB inputs stream from disk instead of loading into memory or a single request.\n- The CLI now recognizes `.zip` files when uploading reusable workflow inputs, so archive uploads get the correct content type without extra flags.\n\n### SDK\n\n[View full release notes →](/releases/0.16.4)\n\n---\n\n## 0.16.3 (2026-09-08)\n\n### Platform (Studio)\n\n- Human review: open pending reviews directly from run details and confirm unchanged fields inline from the field list.\n\n### Public API\n\n- Human review field confirmations now preserve valid JSON `null` values and return clear validation errors when a reviewed value does not match its schema.\n\n---\n\n## 0.16.2 (2026-09-08)\n\n### Platform (Studio)\n\n- Human review: pause workflows and agents only when selected fields need attention, inspect source files beside structured output with spatial citation highlights when lineage is available, correct and confirm values, then approve the same run to continue. Confidence thresholds, explicit field selection, required-field progress, rejection, and cancellation keep exception review focused instead of forcing teams to recheck every field.\n\n### Public API\n\n- Human review APIs now list pending tasks, return review data and source-file references, persist field corrections and confirmations, and approve or reject paused runs with optimistic concurrency protection. Task detail may also include resolved lineage and parsed-document context when an upstream grounded extract produced sidecars.\n\n### CLI\n\n- Workflow authoring now includes human-review guidance and ready-to-use templates for confidence-based exception review and explicit approval of critical fields.\n\n### SDK\n\n- TypeScript and Python SDKs now expose human-review resources for listing and retrieving tasks, confirming corrected fields, downloading attached source files, and approving or rejecting paused runs.\n\n### Infrastructure & processing\n\n- Grounded extraction now runs the same shared algorithm locally when a parse step supplies a valid structured document and your provider supports strict structured output. Step JSON stays schema-shaped with `_grounding` metadata; lineage and the parsed document graph are stored as optional sidecars for review and audit, not mixed into workflow output. Text-only inputs and unsupported providers continue to use the existing text grounding path with clear degraded markers when the shared path is unavailable.\n\n---\n\n## 0.16.1 (2026-09-07)\n\n### Platform (Studio)\n\n- GPT-6 Astra is available across workflows, agents, evaluations, and vision tasks, with configurable reasoning and current token pricing.\n- Self-hosted PaddleOCR models now appear with clear provider identity in Studio model selectors and settings.\n- Archive imports now reject malformed ZIP64 data safely instead of risking an unbounded processing loop.\n\n### CLI\n\n- CLI archive handling now rejects malformed ZIP64 data safely instead of risking an unbounded processing loop.\n\n### Infrastructure & processing\n\n- GPT-6 Astra is available across workflows, agents, evaluations, and vision tasks, with configurable reasoning and current token pricing.\n- Self-hosted deployments can connect directly to PaddleOCR-VL for high-quality document parsing, with bounded concurrency and long-running PDF support built in.\n\n---\n\n## 0.16.0 (2026-09-07)\n\n### Platform (Studio)\n\n- Agent model pickers are now compact, searchable, grouped by provider, and ordered from the most capable model to the least without changing configured defaults.\n- Send Email is now a first-class workflow action. Add Resend or SMTP servers in Settings → Email servers, then pick a stable `ems_…` server on the step — including generated files from the same run as attachments. Cloud and on-prem share this Settings surface; there is no default server. Secrets stay encrypted, and reads never show credentials. Changing an SMTP host, username, or provider asks you to re-enter the password so a stored secret cannot follow a new destination.\n\n### Public API\n\n- The public API now manages organization email servers at `/v1/email-servers`: list, create, inspect, update, delete, and send a connectivity test. Each server is an `ems_…` resource with a Resend or SMTP transport. Secrets are encrypted and never returned; disabled servers reject tests, and names must be unique among live servers. Updating SMTP to a different host or username requires a new password or explicitly clearing authentication — the previous password is never reused against a new destination.\n\n### CLI\n\n- Manage organization email servers with `eigenpal email-servers`: list and inspect `ems_…` servers, create or update Resend and SMTP providers (including an optional SMTP CA PEM), delete servers, and send a live test message with `--to`. Secrets are read from stdin, a file, or a secure prompt — never from command-line flags.\n\n### SDK\n\n- TypeScript and Python SDKs now manage organization email servers: list, create, inspect, update, delete, and send a connectivity test. Stored credentials are never returned.\n\n### Infrastructure & processing\n\n- Send Email delivers through the organization server selected on the step. There is no fallback: a missing, deleted, or disabled server fails the run. Evaluation runs skip delivery, including workflows invoked from an eval. Cancelling a run aborts an in-flight send. Multi-tenant SMTP servers must resolve to public addresses, while single-tenant deployments can use internal relays. Partial SMTP acceptance reports which recipients were delivered or rejected so operators can avoid duplicate reruns. SMTP may still deliver twice if a worker stops after the relay accepts the message; Resend deduplicates the same step attempt.\n\n---\n\n## 0.15.1 (2026-09-04)\n\n### Infrastructure & processing\n\n- OpenParser OCR capacity now recovers cleanly after failed updates, avoiding unnecessary warm resources while preserving active document processing.\n\n---\n\n## 0.15.0 (2026-09-04)\n\n### Platform (Studio)\n\n- Workflow validation now explains when a catalog model belongs in `provider` instead of the raw `model` override, and reruns preserve clean original filenames in generated outputs and citations.\n- Make document inputs uploaded in Studio datasets available to experiment runs, including examples saved by earlier releases.\n\n### Public API\n\n- Experiment result responses and JSON exports now include evaluator-specific structured details, so clients can identify the exact JSON path, expected value, and actual value behind a failed comparison. CSV exports retain their existing stable columns.\n\n### CLI\n\n- Experiment analysis is now built into the CLI for workflows and agents: summarize pass, fail, error, and score totals; filter failures by evaluator; inspect structured discrepancy paths; and compare actual outputs between batches. Run payloads can be projected with `--select`, and repository-level validation discovers nested workflow projects automatically. Polling commands also expose consistent wait limits and reject flag combinations that cannot produce a reliable verdict. Waiting commands now keep human output concise by default; pass `--json` whenever scripts need the terminal payload.\n- `eigenpal docs` now lists, searches, and reads the complete release-matched platform documentation offline, including detailed evaluator syntax, workflow step guides, CLI reference, and the OpenAPI contract.\n- CLI file uploads now send valid content types for Outlook and unknown files, cancellation confirms the asynchronous request clearly, and run comparisons show every repeated workflow step by branch and iteration path.\n\n### Infrastructure & processing\n\n- Make document inputs uploaded in Studio datasets available to experiment runs, including examples saved by earlier releases.\n- Exact-diff evaluators can exclude volatile fields with `ignore: true`, including nested fields inside a selected object or array. This keeps golden outputs readable while scoring only stable business data such as identities, names, and classifications.\n- Script steps now bind inputs by parameter name, so workflows behave consistently after storage even when JSON key order differs from the authored YAML.\n\n[View full release notes →](/releases/0.15.0)\n\n---\n\n## 0.14.3 (2026-09-04)\n\n### Platform (Studio)\n\n- Model selection now includes the GPT 5.6 Sol, Terra, and Luna lineup plus Claude Fable 5, Fable 5.1, Opus 5, and Sonnet 5. Reasoning effort can be configured for supported workflow steps, agent training and runs, workflow-building chat, and LLM judges; existing choices keep their current provider defaults when the setting is omitted.\n\n### CLI\n\n- Workflow YAML can configure model-aware reasoning effort on AI steps and LLM judges. Omit the setting to preserve each model's current default.\n\n### Infrastructure & processing\n\n- AI processing supports the latest GPT and Claude request formats, including reliable structured extraction with Claude Fable 5 and Fable 5.1. Workflow and agent execution can pass an optional model-aware reasoning effort while preserving previous defaults, and agent sandboxes can use GPT 5.6, Claude Fable 5, Fable 5.1, Opus 5, and Sonnet 5.\n\n---\n\n## 0.14.2 (2026-09-02)\n\n### Infrastructure & processing\n\n- Single-node deployments can now upgrade installations with large legacy datasets without startup failures or loss of trigger configuration.\n\n---\n\n## 0.14.1 (2026-09-02)\n\n### Infrastructure & processing\n\n- Single-node deployments can now upgrade installations with legacy run review data without startup failures.\n\n---\n\n## 0.14.0 (2026-09-02)\n\nThis release gives document teams more control over structured-data quality.\nExact-diff evaluators can express field-specific matching rules, PDF parsing can\npreserve visual layout, and spreadsheet conversion can retain the rows, columns,\nand displayed values that matter to downstream workflows.\n\nCLI authoring guidance makes these capabilities easier to discover and safer to\nautomate, with non-interactive commands failing clearly instead of waiting on\nunavailable prompts.\n\n### Platform (Studio)\n\n- `ai.parse` adds `outputFormat: layout` for native PDF extraction. It returns fixed-width plain text that preserves columns, spacing, and line breaks for multi-column forms and tables that lose meaning when flattened. This PDF-only format must be selected explicitly with `parseMode: native` or `parseMode: native-or-ocr`: native mode fails closed if layout extraction cannot complete, while hybrid mode keeps accepted layout pages and substitutes OCR markdown only for suspect or empty pages. Existing formats and defaults are unchanged.\n- Spreadsheet to JSON now handles `.xls` and `.xlsx` workbooks with configurable headerless input or an explicit header row, named or positional column projection, rectangular ranges, and raw or displayed cell values. Displayed values preserve formatted text such as dates, leading zeros, punctuation, diacritics, and embedded newlines. Blank-cell and blank-row policies make sparse sheets predictable, while optional sheet metadata and diagnostics explain the selected range and non-fatal issues. Existing zero-config output remains `rows`, plus `fileId` when CSV output is enabled; zero-config CSV keeps the full selected sheet, and configured CSV follows the same projection. File, row, column, and cell limits reject unsafe workloads before conversion.\n- Exact-diff evaluators add composable per-field rules for extraction outputs. Choose ordered or unordered comparison independently from `items: at-least | at-most | exactly`, set numeric tolerances, and reject unexpected object fields where strict output shapes matter. Ordered arrays retain prefix-compatible matching, while unordered arrays find the best one-to-one structural pairing instead of depending on row order. Unordered object arrays can also use typed `matchBy` identities such as `sku` or `[country, sku]` for predictable single- or composite-key matching. Studio now provides a rule editor with path-aware validation. Grounding metadata is excluded from scoring, oversized comparison workloads fail safely with bounded diagnostics, and existing evaluator configurations keep their historical behavior.\n\n### Public API\n\n- Template creation now accepts reusable file references correctly, restoring CLI template uploads and workflow pushes that use local DOCX or XLSX files.\n\n### CLI\n\n[View full release notes →](/releases/0.14.0)\n\n---\n\n## 0.13.2 (2026-09-01)\n\n### CLI\n\n- CLI installation no longer downloads packages already included in its self-contained executable, reducing install overhead and improving compatibility with restricted enterprise networks.\n\n---\n\n## 0.13.1 (2026-08-31)\n\n### Platform (Studio)\n\n- Workflows can turn JSON rows into a downloadable Excel workbook with a new JSON to XLSX step. Pick ordered columns, map an array of objects, and get a `.xlsx` run output — including multi-sheet workbooks — without filling a template.\n- Studio now uses the shared theme provider, so system / light / dark stays aligned with OpenParser instead of drifting independently.\n- Publishing a workflow that still has a local `template: ./file.xlsx` path now fails with a clear error. Use `eigenpal workflow push` to upload the file, or set `templateId` to a `tmpl_…` id first.\n- Studio template uploads and replacements now create immutable, checksum-backed revisions and discover tokens consistently across DOCX and XLSX files. Deleting a logical template preserves revision content used by pinned workflows.\n- Fill Template (`transform.template`) now documents workspace `tmpl_...` templates separately from Git agent templates, and Studio copy covers DOCX and XLSX. XLSX files use `\\{placeholder}` and `\\{table:array.prop}` in the spreadsheet; `\\{\\{placeholder}}` in an uploaded sheet warns on upload and fails at fill instead of leaving leftover braces. `templateId` must be a workspace `tmpl_...` ID, not a file ID. Filled values that look like formulas stay text; formulas already in the template stay formulas.\n- Studio now blocks crafted HTML from retaining executable content during sanitization.\n\n### Public API\n\n- Workflow push can fully remove a template it uploaded if publishing then fails. Deleting a template that workflows already pin still keeps those revisions runnable.\n- The public API now lists the text, vision, and OCR models configured for your environment. `GET /v1/models` is a cheap catalog inventory for the current tenant: it reports whether each model is configured and which capability it is the default for, without probing live providers or exposing secrets.\n- The public API now supports the full lifecycle for tenant-scoped DOCX and XLSX templates: upload, inspect, list, download, replace, and delete, with direct-upload support for larger files. Every replacement creates a checksum-backed immutable revision; deleting the logical `tmpl_…` resource preserves revisions already pinned by workflows.\n\n### CLI\n\n- `workflow versions create --file` now resolves local `template:` paths the same way as `workflow push`, and `workflow templates smoke --data` help documents the JSON fixture file path.\n- `eigenpal workflow push` keeps local Office templates inside the workflow project, including when a path uses `../` or a symlink. Files outside that directory need `--allow-external-templates`. Push validates the workflow and version before uploading, creates new template resources instead of moving shared `tmpl_…` pointers, and fully removes those unpublished uploads if publish does not succeed.\n- When resolving a workflow by `wf_…` id, connection failures, auth errors, and server outages now surface their normal CLI messages instead of being misreported as \"workflow not found.\"\n\n[View full release notes →](/releases/0.13.1)\n\n---\n\n## 0.13.0 (2026-08-25)\n\n### Platform (Studio)\n\n- API reference for listing dataset examples now documents `include=metadata`, so you can page through example ids, names, and file refs without loading every payload.\n- Local Studio development no longer keeps a growing on-disk compiler cache that can exhaust memory during long `bun dev` sessions.\n- Studio's sidebar theme picker now supports keyboard navigation across system, light, and dark.\n- Studio dashboard sessions continue to authenticate `/api/v1` calls. Token-based `/api/v1` and `/v1` requests still work without a browser login, and the dedicated API hostname keeps its public routing and CORS behavior.\n- Studio save-as-release still publishes a tagged version as the live workflow. Publishing from an existing snapshot creates a new tagged release instead of rewriting the original tag, so earlier releases stay intact. Connect API docs version pickers follow the public version list, so after restore the current untagged version shows as latest while tagged releases stay selectable.\n\n### Public API\n\n- List dataset examples without pulling every payload. `GET /api/v1/automations/:id/examples` now accepts `include=metadata`, which returns ids, names, annotations, and expected file refs while leaving input and expected JSON unloaded. Keep the default `include=full` when you still need the complete example bodies.\n- You can now create, promote, and restore workflow versions through the public API. Create a tagged candidate with `activate: false` to test it without changing live traffic, then promote that exact version after it qualifies. You can also publish from YAML, copy an existing version into a new release without changing its original tag, or restore an earlier snapshot. Name-conflict errors include `conflictingWorkflowId` so you can see which workflow already owns the name.\n\n### CLI\n\n- `eigenpal workflow versions` can now create, promote, and restore YAML workflow versions through the public API. Tag a candidate from a YAML file or an existing snapshot with an explicit semver, keep it off live traffic with `--no-activate`, then promote that exact version when it qualifies. Restoring still rolls a workflow back by copying a snapshot into a new current version — lists now show that untagged HEAD clearly instead of hiding it. `workflow versions list --limit` and `--offset` page that full public list locally. `--json` prints the sliced `\\{ data, total, limit, offset }` envelope (`total` is the unsliced count), not the raw server payload.\n\n### SDK\n\n- TypeScript and Python SDKs can list dataset examples with `include: \"metadata\"`. That returns ids, names, annotations, and expected file refs without loading each example's input and expected JSON. The default remains a full payload, so existing list calls stay unchanged.\n\n[View full release notes →](/releases/0.13.0)\n\n---\n\n## 0.12.2 (2026-08-21)\n\n### Platform (Studio)\n\n- Document editing: the DOCX editor used for templates, run file review, and Office file previews now follows your light or dark theme, and Save (including Cmd/Ctrl+S) writes back to the document you opened instead of downloading a copy. Documents that fail to open now report the reason instead of showing an empty editor. Placeholder highlighting in DOCX templates is temporarily unavailable.\n- Connect: a workflow that wraps another workflow now documents the full response shape. Its OpenAPI spec, Swagger UI preview, and the response examples and generated types on the Connect page show the nested objects and arrays returned by the workflow it calls, instead of leaving those fields untyped.\n\n---\n\n## 0.12.1 (2026-08-19)\n\n### Platform (Studio)\n\n- Connect: download an OpenAPI spec (swagger.json) and preview it in Swagger UI directly from the Connect page. Selecting a workflow or agent produces a spec for just that automation: the five endpoints needed to integrate it (start a run, poll it, cancel it, list and download artifacts), with the request body documenting its exact input fields and file uploads and run responses typed with its actual output schema. With no selection you get the full platform API spec. Swagger UI is bundled with the product, so the preview also works in on-prem installs without internet access.\n\n---\n\n## 0.12.0 (2026-08-18)\n\n### Platform (Studio)\n\n- The workflow builder chat honors the new `disableTemperature: true` model configuration option, so models that reject a sampling temperature work from the first request.\n- Object-typed workflow inputs can now declare a nested schema. In the workflow builder, choosing the Object type (or an array of objects) opens a property editor where you define each field with its type, description, allowed enum values, and required flag, including nested objects and arrays up to five levels deep. Declared fields are enforced when a run starts: missing required fields and type mismatches are rejected with precise field paths. Downstream steps get full autocomplete and type resolution for nested references such as `\\{\\{ input.metadata.vendor.name }}`, and the declared shape appears in the scope browser and in generated API documentation. The builder flags expressions that reference undeclared nested fields, the run form and evaluation input editor pre-fill the declared required fields, and the AI expression generator sees the nested shape when suggesting expressions. The headless execution server now enforces declared inputs with the same validator, so its API documentation and runtime behavior agree.\n- Grounded OpenParser extraction results can now carry complete field lineage through the Eigenpal web runtime, preserving source evidence and confidence metadata for downstream review experiences.\n\n### Public API\n\n- Object-typed workflow inputs can now declare a nested schema. In the workflow builder, choosing the Object type (or an array of objects) opens a property editor where you define each field with its type, description, allowed enum values, and required flag, including nested objects and arrays up to five levels deep. Declared fields are enforced when a run starts: missing required fields and type mismatches are rejected with precise field paths. Downstream steps get full autocomplete and type resolution for nested references such as `\\{\\{ input.metadata.vendor.name }}`, and the declared shape appears in the scope browser and in generated API documentation. The builder flags expressions that reference undeclared nested fields, the run form and evaluation input editor pre-fill the declared required fields, and the AI expression generator sees the nested shape when suggesting expressions. The headless execution server now enforces declared inputs with the same validator, so its API documentation and runtime behavior agree.\n- Listing runs with `sinceLastClosed=true` works again. Once a review had been closed, asking for the runs recorded since then returned an error instead of the runs, so review tooling that polls for new work had no way to ask \"what has arrived since I last signed off\". The filter now returns those runs.\n\n### Infrastructure & processing\n\n- Model entries in the deployment configuration can now set `disableTemperature: true`. Workflow steps then never send a sampling temperature to that model, which lets reasoning models (and gateways in front of them) that reject the parameter run without a first-call retry.\n- Object-typed workflow inputs can now declare a nested schema. In the workflow builder, choosing the Object type (or an array of objects) opens a property editor where you define each field with its type, description, allowed enum values, and required flag, including nested objects and arrays up to five levels deep. Declared fields are enforced when a run starts: missing required fields and type mismatches are rejected with precise field paths. Downstream steps get full autocomplete and type resolution for nested references such as `\\{\\{ input.metadata.vendor.name }}`, and the declared shape appears in the scope browser and in generated API documentation. The builder flags expressions that reference undeclared nested fields, the run form and evaluation input editor pre-fill the declared required fields, and the AI expression generator sees the nested shape when suggesting expressions. The headless execution server now enforces declared inputs with the same validator, so its API documentation and runtime behavior agree.\n\n---\n\n## 0.11.3 (2026-08-13)\n\n### Platform (Studio)\n\n- API reference pages for starting and reading runs now show complete examples, including `output` and `execution.status`.\n\n### Public API\n\n- Run API: completed-run examples now include `type`, `execution.status`, `output`, `files`, and `error`, instead of a shared async/completed union that dropped those fields. Default GET and list responses populate `execution.retry.nextRun` when a retry exists, and `expand=execution` keeps `schemaValid` instead of clearing it. Child invoke-workflow runs document `parentExecutionId` and `childExecutions`.\n\n### SDK\n\n- Generated run types now distinguish a completed run from the async accepted envelope (`RunAccepted`), and include `parentExecutionId` for child invoke-workflow runs.\n\n---\n\n## 0.11.2 (2026-08-07)\n\n- Internal improvements.\n\n---\n\n## 0.11.1 (2026-08-07)\n\n### Platform (Studio)\n\n- Document and image processing now runs on updated imaging components that carry upstream security fixes.\n\n### Infrastructure & processing\n\n- Document and image processing now runs on updated imaging components that carry upstream security fixes.\n\n---\n\n## 0.11.0 (2026-08-06)\n\n### Platform (Studio)\n\n- Workflow builder chat now works with reasoning models served through a self-hosted `openai-compatible` gateway, instead of failing with a \"temperature is not supported\" error. On models that only accept their own default temperature setting, replies can vary between attempts on the same question.\n- Canonical API hostnames now consistently expose only supported API and operational routes, returning a clear 404 for unsupported discovery paths.\n- Agent Builder attachments now negotiate multipart or storage-direct transfer, so large PDFs and archives can reach the active sandbox without crossing cloud payload limits. Large downloads use the same secure dual path. Self-hosted operators can set `deployment.multipartMaxBytes` in `eigenpal.config.yaml`; the supplied on-prem deployments keep files on multipart and streamed downloads by default.\n- Canonical API hostnames now serve API routes and discovery endpoints reliably without Studio sign-in handling intercepting requests.\n\n### Public API\n\n- Upload and download reusable files up to 100 MiB through one negotiated API: files within the deployment body limit use multipart, while larger cloud transfers use verified storage-direct uploads and short-lived authenticated download links. Retry-safe temporary run and Builder files expire after 24 hours. Existing multipart routes and `studio.eigenpal.com/api/v1` remain compatible, and new integrations can use `api.eigenpal.com/v1`.\n\n### CLI\n\n- The CLI now uses `api.eigenpal.com` as the default cloud API origin and automatically pre-uploads run files that exceed the multipart request limit. Self-hosted users can keep custom API origins and set `EIGENPAL_MULTIPART_MAX_BYTES` to their proxy limit, or `none` to keep all files on multipart.\n\n### SDK\n\n- The TypeScript and Python SDKs now transparently upload large run inputs without crossing cloud request limits, follow secure storage-direct downloads, and default cloud calls to `api.eigenpal.com`. Set `multipartMaxBytes` / `multipart_max_bytes`, or `EIGENPAL_MULTIPART_MAX_BYTES`, to match a self-hosted proxy; use `null` / `None` to keep every run file on multipart. Explicit `files.upload` files remain reusable until deleted.\n- TypeScript file uploads now follow the server-negotiated multipart endpoint, improving compatibility across hosted and self-managed route layouts.\n\n[View full release notes →](/releases/0.11.0)\n\n---\n\n## 0.10.56 (2026-08-05)\n\n### Platform (Studio)\n\n- Agent Builder training chat now shows Pierre file diffs for edit/write, a Pierre syntax-highlighted file view for Read file, secure request-secret password cards (encrypted into the draft; the agent only sees configured/skipped), and a quiet timeline note when you release an agent version from the dashboard.\n\n### Infrastructure & processing\n\n- Production releases that do not change document processing finish faster by reusing verified OCR provider checks from the last successful release, while still re-checking providers when processing code or infrastructure changes.\n\n---\n\n## 0.10.55 (2026-08-05)\n\n### Platform (Studio)\n\n- Agent Builder now resumes your latest draft, clearly distinguishes unsaved edits from saved changes that are not live, and provides direct Save and Release actions. Releases show the current and next version and let you choose a patch, minor, or major update. Dashboard save and release actions remain available to the builder as private context, so it can accurately answer what is live without guessing. Starting a separate draft is explicit, attachments remain available when beginning a conversation, stopped sandboxes can be restarted without losing the draft, and test runs reliably receive JSON-only inputs.\n\n### Public API\n\n- Agent Builder releases now complete live automation synchronization reliably, so a successful release is immediately available without a separate recovery step.\n\n### Infrastructure & processing\n\n- OCR and LLM model catalogs keep loading when the service adds new optional fields, so Studio and Eigenpal no longer fall back to stale model lists during catalog updates.\n- Cloud document processing providers that are already available stay online during routine production updates when their identity is unchanged. New or changed providers stay unavailable until they pass live verification, so customers keep working providers instead of a full outage window.\n\n---\n\n## 0.10.54 (2026-08-04)\n\n### Platform (Studio)\n\n- Workflow Studio now shows each OpenParser OCR model with its actual provider logo, keeps narrow step configuration panels contained, and places settings, theme, and sidebar controls together in a cleaner navigation footer.\n\n---\n\n## 0.10.53 (2026-08-04)\n\n### Platform (Studio)\n\n- OpenParser Studio now visualizes the `openparser@1` document graph directly, including typed elements, figures, tables, relationships, and element-level extraction citations.\n\n### Public API\n\n- OpenParser parse responses now return the unified `openparser@1` document graph, with plain text, canonical Markdown, pages, typed elements, relations, annotations, assets, geometry, confidence, and provider provenance in one predictable contract.\n\n---\n\n## 0.10.52 (2026-08-04)\n\n### Platform (Studio)\n\n- Agent Builder: improve connection recovery so interrupted sessions resume reliably, stale environments are replaced safely, and approval prompts remain retryable when delivery fails.\n\n---\n\n## 0.10.51 (2026-08-03)\n\n- Internal improvements.\n\n---\n\n## 0.10.50 (2026-08-03)\n\n### Platform (Studio)\n\n- OpenParser Studio: pipeline creation no longer reports a duplicate-name error after a successful save, and edits now save reliably from the browser.\n\n---\n\n## 0.10.49 (2026-08-03)\n\n- Internal improvements.\n\n---\n\n## 0.10.48 (2026-07-30)\n\n### Platform (Studio)\n\n- OpenParser Studio now displays canonical OpenParser JSON with readable indentation, including large document results.\n\n---\n\n## 0.10.47 (2026-07-30)\n\n### Platform (Studio)\n\n- OpenParser Studio now keeps large document results responsive, renders OCR markdown with clearer structure, and provides consistent copy and download actions for Markdown and JSON output. Documentation links now reliably open the generated API reference.\n\n### Infrastructure & processing\n\n- OpenParser jobs now recover more safely when document processing capacity becomes unavailable. Retryable failures pause new dispatches, retry without duplicate charges, and bound large-document processing so one document cannot overwhelm the service.\n\n---\n\n## 0.10.46 (2026-07-30)\n\n### Platform (Studio)\n\n- Team settings: bring back the invitation link. Creating an invitation now shows its link straight away whenever no invitation email was delivered, and administrators can pull up the link again at any time from the invitations table. This unblocks onboarding on deployments without email delivery, where handing over the link is the only way to get someone into the workspace.\n- Run details: field grounding now labels flagged fields by what the extraction actually reports, showing \"3 with low confidence\" instead of \"3 to review\". The count is a confidence signal, not an assigned task.\n\n---\n\n## 0.10.45 (2026-07-29)\n\n### Platform (Studio)\n\n- Parse Document setup now clearly separates OCR from Vision AI, automatically offers available OpenParser OCR models, shows each model's per-page price, displays only the controls that affect the selected method, and surfaces structured layout data with tables, figures, polygons, and bounding boxes for downstream workflow steps.\n\n### CLI\n\n- The workflow authoring reference now covers explicit OCR and Vision AI parse modes, optional figure-caption models, and structured layout output.\n\n### Infrastructure & processing\n\n- Document parsing now defaults to higher-fidelity OCR, preserves canonical structured layout alongside Markdown, and automatically supports available OCR models with provider-specific per-page pricing. Other OCR providers retain their existing rates.\n\n---\n\n## 0.10.44 (2026-07-28)\n\n- Internal improvements.\n\n---\n\n## 0.10.43 (2026-07-27)\n\n- Internal improvements.\n\n---\n\n## 0.10.42 (2026-07-27)\n\n- Internal improvements.\n\n---\n\n## 0.10.41 (2026-07-27)\n\n### Platform (Studio)\n\n- Run traces now show each attempt of an AI request. A request that timed out and was retried used to appear as a single long bar, so a step that spent three timeouts on one page looked identical to one slow call. The raw trace now has one bar per try, labelled with the try number, and the run timeline marks a retried call with how many times it was sent. Retried requests also record how long they waited for a free slot, which distinguishes a slow model from a queue backed up behind its own in-flight requests. For documents parsed page by page, each split retry records the pages it covered and how deep the split went.\n- Runs and Experiments improvements: - An experiment run now shows the same input and output panels as the main run view, including input file downloads, per-step output files, and inline field-level output review. - Scored runs show their eval score in the runs list and in the run header, so an experiment's results can be compared without opening each run. - A run that came from an experiment or a dataset example now links straight to that run in the Evals view. - The Live and Experiments toggles on Runs are remembered between visits. - Field grounding: a wide document no longer overflows the panel. Wide tables and long unbroken text scroll inside the document card.\n\n---\n\n## 0.10.40 (2026-07-25)\n\n### Infrastructure & processing\n\n- Processing reliability on busy deployments: health checks are now served independently of document processing, so the service stays responsive to them even while working through a heavy document. It is no longer restarted mid-run just because it is under load, so long-running document runs complete more reliably.\n- Document processing is more resilient on CPU- and memory-constrained deployments. Page rasterization for vision and OCR now runs in isolation, so a single large or malformed document can no longer stall the processing service or cause it to restart mid-run. Runs on big scanned documents that previously failed partway through with \"the worker stopped before this attempt recorded a terminal result\" now complete reliably.\n\n---\n\n## 0.10.39 (2026-07-25)\n\n### Platform (Studio)\n\n- Label Pages runs: the page preview now shows the classified document in the full viewer instead of small thumbnails. PDFs render page-by-page with zoom and navigation and each page's labels shown beside it; Word and other Office documents open in their own preview next to a per-page label list. Filter by a label to focus on just the pages that carry it.\n\n---\n\n## 0.10.38 (2026-07-24)\n\n### Platform (Studio)\n\n- OpenParser Studio: manage extraction pipelines from a dedicated workspace — create, edit, run, duplicate, and delete saved configurations. Jobs show the pipeline and version that produced each result, and you can save a new pipeline from job settings.\n- OpenParser Studio: clearer progress while documents upload, parse, and extract, with live elapsed timing at each stage. Job history and the playground share a unified header with breadcrumbs, improved document and Markdown output viewers, and a cleaner parse sidebar. Choose light or dark theme and manage your account from the sidebar; model selectors and in-product copy are simpler throughout the workspace.\n- Members provisioned through SSO are now read-only in workspace member settings. Their role and access come from your identity provider's groups, so admins can no longer change or remove them by hand, and each SSO member is clearly labeled. Accounts created with a username and password stay fully manageable by admins.\n\n### Public API\n\n- **Breaking:** OpenParser API resource identifiers now use consistent prefixed ids. Parse, extract, and batch jobs return `opj_…` ids instead of UUIDs. Saved extraction pipelines use `oppl_…` instead of `oep_…`. New parse output writes figure image ids as `opfig_…` instead of `ocrfig_…`. Existing jobs and pipelines are remapped automatically on upgrade when the OCR queue is idle enough for migration preflight. Prefer the new formats in job URLs, `parse_job_id`, `pipeline_id`, and batch child `job_id` fields. During the transition, previously issued UUID job ids, `oep_…` pipeline ids, and UUID list cursors remain readable; responses continue to return the preferred `opj_…` / `oppl_…` forms. Historical parse artifacts and `result_json` may still embed legacy `ocrfig_…` figure paths; `GET /files/\\{id}` accepts both `opfig_…` and `ocrfig_…`.\n\n### Infrastructure & processing\n\n- Hardened AI-powered document processing against prompt-injection attempts hidden inside uploaded documents. Instructions embedded in a document can no longer steer extraction, classification, or vision results away from the requested schema, or coax the model into disclosing its own configuration.\n- AI vision step results no longer include internal document storage references in their metadata. The step's `_vision` provenance now records only which pages were inspected and how many were merged, keeping internal storage paths out of workflow output.\n\n---\n\n## 0.10.37 (2026-07-24)\n\n### Platform (Studio)\n\n- New **Label Pages** step tags each page of a document with zero or more labels (multi-label) using AI, and groups the results into a label-to-pages map. Unlike section splitting — which produces one contiguous range per section — this collects pages of the same kind wherever they appear, so scattered pages (for example every signature or photo page throughout a report) can be gathered in one place. The page-inspection step can now take an explicit list of page indices, so you can route the pages for a given label straight into a visual inspection without hand-listing them.\n\n---\n\n## 0.10.36 (2026-07-23)\n\n### Platform (Studio)\n\n- OpenParser Job History now reports the real end-to-end duration for completed parse and extraction jobs instead of showing a zero or unavailable measurement.\n- Running OpenParser jobs now show the document scanning animation from the playground while results are being prepared.\n- Parse and Extract now use consistent operation icons across OpenParser Studio and documentation.\n\n---\n\n## 0.10.35 (2026-07-23)\n\n### Platform (Studio)\n\n- OpenParser sign-in and account creation now redirect directly to the branded authentication flow without an intermediate confirmation page.\n- OpenParser parse jobs now retain both `raw` and `openparser@1` results. Retrieve either representation with `GET /jobs/\\{id}/result?format=...`, switch formats in the Studio Output tab, and start extraction from a raw-format parse job without running OCR again.\n\n### Public API\n\n- OpenParser parse jobs now retain both `raw` and `openparser@1` results. Retrieve either representation with `GET /jobs/\\{id}/result?format=...`, switch formats in the Studio Output tab, and start extraction from a raw-format parse job without running OCR again.\n- OpenParser now keeps recognized text aligned with its original bounding box by default. Set `ocr_options.merge_layout_blocks` to `true` per parse, extraction, batch item, or saved pipeline when merged recognition is preferred over one-to-one text geometry.\n\n---\n\n## 0.10.34 (2026-07-23)\n\n### Platform (Studio)\n\n- OpenParser sign-in and account creation now keep OpenParser branding throughout the authentication experience.\n- OpenParser Studio: uploads and sample documents now open their Job History detail immediately, so browser back and forward navigation follows each run naturally while extraction and result review stay in one place.\n- Run detail now shows clean, readable file names for inputs and outputs instead of internal storage identifiers, and long file names no longer overlap the preview and download buttons.\n\n### Public API\n\n- OpenParser parsing now preserves every detected layout region—including empty title, header, and figure boxes—so document overlays remain complete. The obsolete `figure_assets` option is removed because figure regions are now always retained.\n- OpenParser can now process documents beyond 25 pages and admit batches of up to 100 documents, while retaining per-page safety limits.\n- OpenParser jobs can now stream their retained source documents with `GET /jobs/\\{id}/source`, making it easier to build durable review experiences around past runs.\n- `POST /suggest-schema` now returns the suggested schema inline as a convenience response instead of creating a job that clients must poll.\n\n### Infrastructure & processing\n\n- Documents uploaded without a declared file type are now recognized from their contents, so PDFs, RTF, and Office files parse reliably instead of failing with an \"unsupported file type\" error. This most often helped uploads coming from automated integrations that omit the file type.\n\n---\n\n## 0.10.33 (2026-07-22)\n\n### Platform (Studio)\n\n- OpenParser Studio: make Playground the home page, add shareable social previews, clarify job status and pipeline empty states, and tidy the OCR model selector copy.\n\n### Public API\n\n- OpenParser suggested extraction model catalog (`GET /models/llm` in suggested mode) now surfaces up to 20 recommended models instead of 12, so more current options appear before you need to search.\n\n---\n\n## 0.10.32 (2026-07-22)\n\n- Internal improvements.\n\n---\n\n## 0.10.31 (2026-07-22)\n\n### Platform (Studio)\n\n- OpenParser Studio production sign-in no longer auto-redirects to Eigenpal when you are signed out. Choose **Continue to sign in** on the local OpenParser page to authenticate with your Eigenpal account; sign-out stays on OpenParser.\n\n---\n\n## 0.10.30 (2026-07-22)\n\n### Platform (Studio)\n\n- OpenParser production sign-in now shares Eigenpal sessions across its dedicated domain and returns users to their original OpenParser destination after authentication.\n- OpenParser Playground: remove the doubled border and rounded corners around the document workspace.\n\n### Infrastructure & processing\n\n- OpenParser: restore schema suggestions, use GPT-5.6 Terra as the default extraction model with GPT-4.1 Mini as a certified fallback, tighten compatibility checks, and provide clearer failure guidance.\n\n---\n\n## 0.10.29 (2026-07-22)\n\n### Platform (Studio)\n\n- OpenParser Studio sign-in and sign-up now load the shared identity UI correctly for signed-out visitors.\n\n---\n\n## 0.10.28 (2026-07-22)\n\n### Platform (Studio)\n\n- API keys: keep usable credentials in the primary list and move revoked or expired credentials into a collapsed history section for faster scanning.\n- OpenParser Studio sign-in and sign-up now show OpenParser product copy and logo while retaining the shared Eigenpal identity.\n\n### Public API\n\n- OpenParser model discovery (`GET /models/llm`) now refreshes the live compatible catalog on the API service instead of falling back to a short emergency list when the catalog credential is missing from that process.\n\n### Infrastructure & processing\n\n- OpenParser document processing now bounds source downloads and retries transient storage failures, preventing stalled jobs from running indefinitely.\n\n---\n\n## 0.10.27 (2026-07-21)\n\n### Platform (Studio)\n\n- New Inspect Pages (Vision) step lets a workflow look at the actual page images of a document and return structured results, for details that only exist in the pixels rather than the text. Use it to check whether a document is signed, rate embedded photos, or spot stamps and logos. It works on PDFs, images, and Office documents, and you can point it at a specific page range (for example the section found by a Split step) so it only reviews the pages that matter. The run trace shows the pages it inspected next to the results.\n- Parsing can now describe the figures on a page as text. Enable \"describe figures\" on a Parse step and the images on each page (property photos, charts, stamps) are detected and captioned inline, so later steps that work on text, like Split, can find image-only pages. This is especially useful for scanned documents, where the photo pages carry almost no text on their own. You can provide custom instructions for how figures should be described.\n- Split step results are now also available keyed by section name. A later step can point at one section directly, for example `\\{\\{ steps.split.output.sections.photos.page_range }}`, instead of filtering the full list. This makes wiring a Split step into a downstream step much simpler.\n- The Inspect Pages (Vision) step now takes a Page From / Page To range instead of a single page-range expression, and both are optional. Leave them blank to inspect the whole document, which is processed in chunks and the results merged automatically. This makes it much easier to point the step at a range (for example the start and end of a Split section) or to scan an entire document without writing a range expression.\n\n---\n\n## 0.10.26 (2026-07-21)\n\n### Platform (Studio)\n\n- Wildcard API keys can use the OpenParser OCR API consistently with every other platform capability. Keys can also be restricted to the dedicated `ocr:full` scope when OCR-only access is preferred.\n- Run traces now include a Grounding panel for extract steps: every extracted value is highlighted in the input text, tinted by confidence, and clicking a field or a highlight jumps between them. Grounded fields also show inline context previews of the exact source passage, both in the panel and in the expanded step row. Split steps gain the same visual page-range panel the segmenter uses, with page thumbnails and per-section preview. The workflow builder reflects that grounding is on by default and lets you pick any configured provider as the grounding model, and capturing expected output from a run no longer snapshots volatile grounding metadata into evals.\n\n### Public API\n\n- Wildcard API keys can use the OpenParser OCR API consistently with every other platform capability. Keys can also be restricted to the dedicated `ocr:full` scope when OCR-only access is preferred.\n- OpenParser: completed extraction jobs can now be retrieved reliably with their attempt and usage metadata.\n\n### CLI\n\n- The step catalog now documents grounded extraction as the default: extract steps ship per-field confidence and source spans under the `_grounding` output key unless `grounded: false` is set.\n\n### Infrastructure & processing\n\n- Data extraction now verifies itself by default. Every extract step traces each extracted field back to its exact location in the source text and attaches a confidence level (high, medium, or low) plus a needs-review flag under a reserved `_grounding` output key, so downstream steps and evaluators can route uncertain values to a human. The verification pass runs on your workspace model (any configured provider), chunks long documents automatically, and falls back to deterministic text matching instead of failing the run when no model is available. Set `grounded: false` on a step to opt out.\n\n---\n\n## 0.10.25 (2026-07-20)\n\n### Platform (Studio)\n\n- Failed single sign-on attempts now return to the sign-in page with a clear, actionable message (for example, when an account's email domain isn't authorized or the account hasn't been granted access) instead of a blank error page.\n\n---\n\n## 0.10.24 (2026-07-15)\n\n### Platform (Studio)\n\n- Agent runs graded by configured evaluators now record a weighted score and pass/fail result, visible in the runs feed and run detail responses. Previously only workflow runs carried an evaluator verdict.\n- Email addresses for a workflow are unique across your whole organization, not just the workflow you are editing. Picking one that is already taken used to fail with a server error. It now tells you which workflow already receives email at that address, or that the workflow you are editing already has it, so you know what to do next. Duplicate folder names and duplicate workspace invites also report clearly instead of failing.\n- `ai.extract` with `grounded: true` no longer succeeds silently without producing grounding. When the grounding pass cannot run (missing OpenAI credentials on the worker, or a grounding model that is not an OpenAI model) the step now fails with an error that names the exact misconfiguration. When the pass runs but produces no field grounding, the output carries an explicit degraded marker instead of nothing. The grounding model picker in the workflow builder now only offers OpenAI models, matching what the grounding pass supports.\n- Agent training: when a training session fails to open, the dashboard now shows what went wrong instead of a generic error. The message names the setup step that failed, and a session blocked by secrets that cannot be decrypted says so and points at the agent settings. A session whose setup fails partway through no longer opens on an incomplete workspace.\n- Self-hosted deployments can now sign users in through corporate single sign-on services that use a redirect-and-token flow instead of OIDC. The new protocol supports mutual TLS for the server-to-server exchange, maps directory groups to EigenPal roles (admin, member, viewer) on every SSO sign-in so the directory stays the source of truth, and links existing password accounts by email so nobody loses access when SSO is turned on. Startup validation rejects unsafe callback origins and provider identifiers, while malformed or oversized identity responses fail closed.\n\n### Public API\n\n- Syncing an automation immediately after a release no longer fails with HTTP 500 while the new release tag propagates. The sync endpoint now reports a retryable status with a clear message when the Git source is momentarily unavailable.\n- Fixed single-experiment results export returning HTTP 400 whenever the `includeTrace` query flag was omitted. `workflow experiment results` and the auto-pull at the end of `workflow experiment watch` now download per-evaluator results in JSON and CSV again.\n- Creating an email trigger address that is already in use now returns 409 with a message naming the workflow in your organization that already uses it, instead of 500.\n\n### CLI\n\n- Agent example runs now report a real evaluator verdict in the CLI: `eigenpal run agents.<name> --example <name> --wait` waits for the verdict, prints the weighted score and pass/fail result, and `--fail-on-mismatch` exits non-zero when the run fails its evaluators, matching the existing workflow behavior. When no evaluators are configured, the CLI says so instead of silently reporting nothing.\n- `agents secrets` is now one command group with `list`, `set`, `unset`, `import`, and `export` subcommands (`agents secret` remains as an alias). The new `agents secrets list` shows the secret names and descriptions declared in the package without ever printing values. Previously `set` and `unset` lived under a separate singular command and `secrets list` failed with an argument error.\n- `agents sync` retries automatically with a short backoff when the server reports the Git source as temporarily unavailable, so the sync right after `agents release` succeeds without manual retries.\n- `agents dataset validate` now checks the same layout the server accepts on `agents dataset push`: example folders under `examples/`, file inputs referenced from `input.json` with `\\{ \"$file\": \"input/...\" }`, and `expected.json` optional. Previously the local validator demanded a different layout that push would then reject, so no dataset could satisfy both. Validation errors now point at the exact rule that failed, including a hint when example folders sit at the dataset root instead of under `examples/`. When the agent package carries an input schema, each example's full input object is validated against it (required fields, unknown keys, and value types, with `\\{ \"$file\": ... }` references accepted for file fields), so inputs that would fail at run start are caught locally. Failure-expected examples (`expected.json` with `$error`) are rejected for agent datasets, since agent runs are evaluated only when they complete.\n\n[View full release notes →](/releases/0.10.24)\n\n---\n\n## 0.10.23 (2026-07-15)\n\n- Internal improvements.\n\n---\n\n## 0.10.22 (2026-07-14)\n\n### Platform (Studio)\n\n- Organization members can now configure, test, and manage outbound webhooks from Studio.\n\n---\n\n## 0.10.21 (2026-07-14)\n\n### Platform (Studio)\n\n- Webhooks: notify external systems when workflow or agent runs are created or change status, with signed delivery, custom headers, retry history, testing, and manual redelivery from Studio.\n- Webhook signing secrets are now easier to copy securely from endpoint setup and details, with clearer event selection defaults.\n- Webhook setup now keeps failed submissions actionable with clear inline error feedback.\n\n### Public API\n\n- Run creation and rerun requests now publish signed lifecycle webhooks only after inputs are ready, so API-triggered automations never emit events for runs that fail during setup.\n\n### Infrastructure & processing\n\n- Improved staging worker deployment reliability when compute capacity is fully allocated.\n\n---\n\n## 0.10.20 (2026-07-12)\n\n### Platform (Studio)\n\n- Workflows: configure automatic retries from Studio at the workflow level or override them for individual HTTP GET/HEAD and Website Reader steps.\n\n### CLI\n\n- The bundled workflow reference now documents durable retry policies, inheritance, supported step types, and retry safety limits.\n\n### Infrastructure & processing\n\n- Workflow runs can recover replay-safe network steps from transient failures without restarting completed work, using bounded backoff and durable retry state. Retry behavior is now step-scoped. Legacy whole-run retry settings no longer restart a failed workflow; configure durable retries on eligible steps or through the workflow retry default instead.\n\n---\n\n## 0.10.19 (2026-07-09)\n\n### Platform (Studio)\n\n- Workflow composition is unified on `action.invoke-workflow`: use `execution: inline` (default) to run a target workflow's steps inside the parent run, or `execution: child` for a separate child run. Reusable blocks (`control.block`, `kind: block`) are removed — migrate to inline invoke with `workflow: <name-or-id>`.\n\n### Public API\n\n- Workflow run endpoints now preserve inline invoke snapshots across initial runs, reruns, and dataset executions so composed workflows remain reproducible after target workflows change.\n\n### CLI\n\n- Workflow YAML reference docs now describe `action.invoke-workflow` as the single composition primitive, including inline and child execution modes.\n\n---\n\n## 0.10.18 (2026-07-08)\n\n### Infrastructure & processing\n\n- On-prem deployments: improve OpenShift compatibility so migration and processing jobs can start reliably under random-user security policies.\n\n---\n\n## 0.10.17 (2026-07-08)\n\n### Platform (Studio)\n\n- Document parser: vision parsing can now render PDF pages at higher resolution and image quality for documents where small text or compression artifacts hurt OCR accuracy.\n\n### Infrastructure & processing\n\n- Document parser: vision parsing can now render PDF pages at higher resolution and image quality for documents where small text or compression artifacts hurt OCR accuracy.\n\n---\n\n## 0.10.16 (2026-07-07)\n\n### Platform (Studio)\n\n- Workflow experiment results now preserve evaluator error details alongside each per-evaluator score, so example grading views and tools can distinguish failed judges from unknown results instead of treating them as passing.\n\n### CLI\n\n- `eigenpal run <workflow> --example <name>` now actually grades the example instead of printing `PASS` whenever the run merely finished. It runs the workflow's current published version on the server's stored dataset example, so configured evaluators run automatically and the CLI surfaces the real weighted score and per-evaluator pass/fail. Workflows with no evaluators fall back to a structural diff of the output against the example's stored expected output. The summary reports two separate signals: execution health (`ok`/`errored`) and accuracy. Add `--fail-on-mismatch` to exit non-zero when a graded example fails. Because the run uses the server-side example, push your latest dataset and evaluators (`eigenpal workflow dataset push`, `eigenpal workflow evaluators push`) before running `--example`.\n\n---\n\n## 0.10.15 (2026-07-06)\n\n### Platform (Studio)\n\n- Experiments: make result review easier in Studio with trace exports, clickable score bars, calmer live refreshes, developer-view defaults, and clearer separation between below-threshold results and run errors.\n\n### Public API\n\n- Experiment export endpoints now support JSON exports with full run trace details for deeper debugging and audit review.\n\n### SDK\n\n- SDK clients now expose the trace-inclusive experiment export option for deeper debugging and audit review.\n\n---\n\n## 0.10.14 (2026-07-06)\n\n### Platform (Studio)\n\n- Evaluations: expected workflow failures now report cleaner scores when a run is supposed to stop early. Studio marks non-applicable quality judges as skipped, so successful rejection examples can receive a full score from the structured failure checks that actually apply.\n\n---\n\n## 0.10.13 (2026-07-06)\n\n- Internal improvements.\n\n---\n\n## 0.10.12 (2026-07-06)\n\n- Internal improvements.\n\n---\n\n## 0.10.11 (2026-07-03)\n\n- Internal improvements.\n\n---\n\n## 0.10.10 (2026-07-03)\n\n- Internal improvements.\n\n---\n\n## 0.10.9 (2026-07-02)\n\n### Platform (Studio)\n\n- Build document-processing workflows that split, route, and verify a mixed batch: - **Document separation** detects the boundaries and types of the documents inside one large scan, so a single upload can be broken into its individual documents and each sent to its own extraction pipeline — even for batches hundreds of pages long. - **Multi-way routing** sends each detected document to the right branch by its type, and stays readable on the canvas even with many branches. - **Grounded extraction** attaches a confidence level and source location to each extracted field, flagging values that aren't found verbatim in the document so reviewers know where to look. - The run view now lets you open the detected documents and preview their pages, and inspect the OCR layout with bounding boxes drawn over the page (with a toggle to the extracted text) — right from the step timeline.\n\n### CLI\n\n- The workflow step catalog now lists the new document-separation and multi-way routing steps, plus the grounded-extraction options, so authoring and validating workflows from the CLI stays in step with what the platform supports.\n\n### Infrastructure & processing\n\n- Self-hosted (on-prem) deployments can now run official EigenPal release images, with access provisioned per customer. Images are published only after a release ships and are signed so you can verify their authenticity before deploying.\n\n---\n\n## 0.10.8 (2026-07-01)\n\n### Platform (Studio)\n\n- Single-file workflow inputs now display correctly after a dataset import. Previously an input that takes one file could show an empty file slot; imported examples now match each file to the workflow's declared input shape.\n- Evaluator scoring now uses a single pass threshold for the whole workflow. The score-by-example chart's cutoff line and every evaluator's pass/fail are measured against that one threshold, so the chart, the per-evaluator results, and the run gate always agree. Experiment scores are shown in two states — passing (green) or below threshold (amber).\n- The LLM judge evaluator now requires evaluation criteria. Without them the model invents its own scoring rubric, so the editor marks the field required, renames it to \"Evaluation criteria\" with a worked example, and blocks saving until it is filled in.\n- Workflow folders can now be reorganized. A new \"Move to folder\" action in each folder's menu lets you move a folder (and everything inside it) into another folder or back to the top level, so you can restructure your workspace without recreating folders. Destinations that would nest a folder inside itself are blocked, with a tooltip explaining why.\n- Nested sub-workflows now display and resolve correctly across runs, the workflow builder, and the CLI. In the Runs list, a run that invokes a sub-workflow which itself invokes another sub-workflow now nests every level under its parent, instead of stopping at the first level. In the workflow builder, an \"Invoke Workflow\" step that targets a workflow built from other invoked workflows now shows the real output field types instead of \"unknown\", so you can reference those fields with autocomplete; referencing one of those fields no longer triggers a false \"field not found\" warning. The same resolution now applies when validating and publishing workflows (including via the CLI), so deeply nested invoke chains validate against their real output fields. All of this resolves through arbitrarily deep chains with a safety limit that prevents looping on workflows that invoke each other.\n- Runs: add a simplified review mode with developer-view toggle, review status badges, and inline output review for JSON fields and generated files.\n- New debugging and control primitives for runs. You can now cancel an in-progress run through the API, experiment results report when a batch has fully finished, steps skipped by a condition or an empty loop are reported as skipped rather than succeeded, and each step records the exact configuration it ran with so failures are easier to diagnose.\n- Run detail responses are leaner and more explicit: you choose exactly which sections to expand, and the public run schema now exposes the workflow version, cancellation-request time, and trace, retry, and email-trigger details.\n\n### CLI\n\n- `eigenpal workflow validate` now tells you when a workflow contains Invoke Workflow steps that local validation cannot fully check. Because those steps reference other workflows that live on the server, the command prints a short note pointing you to `validate --online` (or push) to verify the invoke targets, input types, and cycles. The skill guidance now shows the `--online` step in the validate-then-push flow as well.\n- The evaluator configuration reference now reflects the single workflow-level pass threshold. A per-evaluator `passThreshold` is documented as legacy — pass/fail is decided by the one workflow-level threshold — and that workflow-level threshold no longer shows a fixed default, since it is derived when omitted.\n\n### SDK\n\n- The Python SDK now requires Python 3.10 or newer. Python 3.9 has reached the end of its support window, and dropping it keeps the SDK on current, actively maintained dependencies. If you are still on Python 3.9, upgrade to 3.10+ to continue receiving SDK updates.\n\n[View full release notes →](/releases/0.10.8)\n\n---\n\n## 0.10.7 (2026-06-30)\n\n### Platform (Studio)\n\n- Faster dashboard navigation. Lists keep their contents on screen while filters and folders refresh instead of flashing a loading state, document and image previews are cached so they no longer re-download on every navigation, and heavy viewers (PDF, charts, rich text) load on demand for a quicker first paint.\n- Workflows that invoke another workflow now pass file inputs through to the sub-workflow, so file-backed sub-workflow runs no longer fail before they start.\n\n### CLI\n\n- `eigenpal --help` now groups `run`, `rerun`, and `runs` under a dedicated \"Run\" section instead of the catch-all \"Other\" bucket, so starting a run and reading its output is easy to find.\n\n---\n\n## 0.10.6 (2026-06-30)\n\nThis release contains maintenance and reliability improvements with no customer-facing changes.\n\n---\n\n## 0.10.5 (2026-06-29)\n\nThis release makes EigenPal's public changelog a reliable product record. Each release now has a dedicated docs page that explains what changed for Studio, the public API, the CLI, SDKs, and document processing in one place.\n\nThat means teams can follow platform improvements without piecing together GitHub activity or package-specific notes. The changelog is organized around customer-facing product areas, so it is easier to see which changes affect app users, API integrations, developer tools, and processing behavior.\n\n### Platform (Studio)\n\n- Invoke Workflow steps are now typed end to end. Pick a target workflow and map each input it declares to a value, then reference the workflow's output fields and any files it generates from downstream steps with autocomplete. Inputs and outputs are checked when you save and when the step runs, so mismatches surface early instead of failing mid-run.\n- Runs that invoke another workflow now appear nested under their parent run in both the runs feed and the run timeline, and link straight to the sub-workflow's own run and trace.\n- You can view a workflow's inferred output schema (the fields and files it returns, typed from its own steps) without running it.\n- File inputs are preserved consistently across runs, reruns, reviewed examples, and dataset experiments, so reused runs and examples no longer lose or mispoint their files.\n- Creating new organizations and other Git-backed source operations stay reliable even during Git provider outages.\n\n### Public API\n\n- Run file inputs now use a consistent artifact model. Callers provide reusable files with `$fileId` or inline files with `$inline`, and EigenPal materializes them into run-owned artifacts before execution, reruns, promotion, and dataset workflows.\n- A new endpoint returns a workflow's inferred output schema without running it, as JSON Schema or as a TypeScript or Python type.\n\n### CLI\n\n- `eigenpal workflow schema <workflow>` prints a workflow's inferred output schema as JSON Schema, or as a TypeScript or Python type with `--format`.\n\n[View full release notes →](/releases/0.10.5)\n\n---\n\n## 0.10.4 (2026-06-25)\n\n### Platform (Studio)\n\n- Unify file list review surfaces\n\n---\n\n## 0.10.3 (2026-06-24)\n\n### SDK\n\n#### Patch Changes\n\n- e48cb81: Remove `fileVerdict` from run review correction types; file correction intent is encoded via `correctedArtifactPath`.\n\n---\n\n## 0.10.2 (2026-06-24)\n\n### CLI\n\n#### Patch Changes\n\n- ca9ae4f: Workflow inputs can declare an external file `source` (single-tenant on-prem). A `type: file` input with `source: <name>` (e.g. `gpfs`) is provided as a plain string id at run time; the worker resolves it to a file artifact via the configured source before the workflow executes.\n\n---\n\n## 0.10.1 (2026-06-23)\n\n### Public API\n\n- Polish reviews pages and group API reference nav\n\n### Infrastructure & processing\n\n- Client-shareable deployment architecture PDFs\n\n---\n\n## 0.10.0 (2026-06-23)\n\n### Platform (Studio)\n\n- Add simple runs review mode\n\n### Infrastructure & processing\n\n- Autogenerate chart architecture diagram from helm template\n- AWS/GCP/Azure Terraform examples with architecture diagrams\n- Helm chart + local kind for single-tenant on-prem\n\n---\n\n## 0.9.0 (2026-06-22)\n\n### CLI\n\n#### Minor Changes\n\n- c35d5d4: Clean up the public runs and evaluation API surface: move workflow eval operations onto automations, expose run scores/artifacts through the unified runs API, and update CLI/SDK helpers to use the canonical routes.\n\n Breaking: `runs.evalResults` and `GET /api/v1/runs/\\{id}/eval-results` are replaced by `runs.scores` and `GET /api/v1/runs/\\{id}/scores`.\n\n#### Patch Changes\n\n- c35d5d4: Clarify `--max-wait` help text so timeout exit behavior reads naturally.\n\n### SDK\n\n#### Minor Changes\n\n- c35d5d4: Clean up the public runs and evaluation API surface: move workflow eval operations onto automations, expose run scores/artifacts through the unified runs API, and update CLI/SDK helpers to use the canonical routes.\n\n Breaking: `runs.evalResults` and `GET /api/v1/runs/\\{id}/eval-results` are replaced by `runs.scores` and `GET /api/v1/runs/\\{id}/scores`.\n\n[View full release notes →](/releases/0.9.0)\n\n---\n\n## 0.8.0 (2026-06-18)\n\n### CLI\n\n#### Minor Changes\n\n- d620a00: Expose the clean public automation API around automations, runs, files/artifacts, datasets, evaluators, experiments, evaluation results, and run promotion. This intentionally breaks the pre-v1 SDK/CLI surface that exposed workflow- and agent-specific top-level resources outside the explicit legacy compatibility routes.\n\n > [!WARNING]\n > **Breaking change (pre-v1).** The workflow- and agent-specific top-level\n > resources have been removed in favor of a single automation-centric surface.\n > These packages are still 0.x, so this breaking redesign ships as a `minor`\n > bump. Update your integration before upgrading.\n\n #### Removed\n - Top-level workflow resources, e.g. `client.workflows.*`, including\n `client.workflows.executions.runAndWait(...)` and the rest of the\n `client.workflows.executions.*` tree.\n - Top-level agent resources, e.g. `client.agents.*` (runs, reviews, traces,\n files, expected artifacts).\n - Top-level source/git resources, e.g. `client.sources.*`.\n\n[View full release notes →](/releases/0.8.0)\n\n---\n\n## 0.7.2 (2026-06-13)\n\n### SDK\n\n#### Minor Changes\n\n- c5a709c: Public workflow list/detail responses include `apiEnabled` and `triggers`; run definition docs note workflow-only scope.\n\n---\n\n## 0.7.1 (2026-06-13)\n\n### SDK\n\n#### Patch Changes\n\n- 9db3f01: Expose `apiEnabled` and `triggers` on `AgentSummary` for runtime trigger projection.\n\n---\n\n## 0.7.0 (2026-06-12)\n\n### CLI\n\n#### Minor Changes\n\n- 963fd6c: Unify run detail on `GET /api/v1/runs/\\{id}`: return the canonical Run object directly (no `\\{ run: ... }` envelope) and merge expanded fields in-place via documented `expand` tokens. Remove the session-only `expand=internal` dashboard escape hatch; SDKs and CLI now use explicit expand lists.\n\n#### Patch Changes\n\n- 963fd6c: Update runs commands for the grouped run API shape and four expand tokens.\n\n### SDK\n\n#### Minor Changes\n\n- 963fd6c: Unify run detail on `GET /api/v1/runs/\\{id}`: return the canonical Run object directly (no `\\{ run: ... }` envelope) and merge expanded fields in-place via documented `expand` tokens. Remove the session-only `expand=internal` dashboard escape hatch; SDKs and CLI now use explicit expand lists.\n\n#### Patch Changes\n\n- 12c00d8: Docs: clearer wording in the file-input and TypeScript-runtime sections. No API or behavior changes.\n\n---\n\n## 0.6.17 (2026-06-12)\n\n### SDK\n\n#### Patch Changes\n\n- 716c3cf: Update the hosted default API base URL and API-key guidance from `app.eigenpal.com` to `studio.eigenpal.com`.\n\n---\n\n## 0.6.16 (2026-06-12)\n\n### CLI\n\n#### Patch Changes\n\n- 75dd64c: Internal refactor: move the skill tool registry into its own module so other tooling can import it. No change to any command, flag, argument, or output.\n\n---\n\n## 0.6.15 (2026-06-10)\n\n### CLI\n\n#### Minor Changes\n\n- ca87265: Unify workflow and agent run starts behind the canonical `/api/v1/run/\\{target}` endpoint, root `eigenpal run` / `eigenpal rerun` commands, and root SDK `client.run()` / `client.rerun()` methods.\n\n The old nested CLI commands and SDK resource methods for starting workflow or agent runs have been removed.\n\n#### Patch Changes\n\n- 5ea0bb5: `workflow dataset pull` gains `--example-id <id>` (repeatable) to export a single example or a selected subset of a dataset, instead of always pulling the whole dataset. The subset ZIP uses the same archive layout as a full pull, so it re-imports into any dataset via `dataset push --mode append`. Requesting an example id that does not exist for the workflow exits non-zero.\n\n### SDK\n\n#### Minor Changes\n\n- ca87265: Unify workflow and agent run starts behind the canonical `/api/v1/run/\\{target}` endpoint, root `eigenpal run` / `eigenpal rerun` commands, and root SDK `client.run()` / `client.rerun()` methods.\n\n The old nested CLI commands and SDK resource methods for starting workflow or agent runs have been removed.\n\n---\n\n## 0.6.10 (2026-06-08)\n\n### CLI\n\n#### Patch Changes\n\n- 99ca1b4: Unify agent and workflow run commands and SDK helpers on the shared `/api/v1/runs` API, including the public `client.runs` facade and regenerated SDK reference docs.\n\n### SDK\n\n#### Patch Changes\n\n- 99ca1b4: Unify agent and workflow run commands and SDK helpers on the shared `/api/v1/runs` API, including the public `client.runs` facade and regenerated SDK reference docs.\n\n---\n\n## 0.6.9 (2026-06-04)\n\n### SDK\n\n#### Minor Changes\n\n- 5e7c051: Add `observability` (phase timeline + structured failure) to workflow execution and agent run status API responses.\n\n---\n\n## 0.6.6 (2026-06-03)\n\n### CLI\n\n#### Patch Changes\n\n- 245f347: Clean up the agent CLI surface by removing obsolete compatibility commands, moving reruns to `agents rerun`, and replacing run pulls with canonical artifact fetching.\n- 5ab00ee: Tighten permissions on `~/.config/eigenpal/credentials.json` and its parent directory to owner-only (0600 / 0700). Previously the file was written 0600 but the parent directory inherited the user's umask (typically 0755), allowing other local users to list the directory and stat the file. The CLI now also re-tightens permissions on every write, so installs created with older versions are corrected on the next `auth login` / `auth use`.\n\n---\n\n## 0.6.4 (2026-06-03)\n\n### CLI\n\n#### Patch Changes\n\n- c3486a5: Fix agent git cutover QA findings: dashboard runs now navigate to the created run, inbound email aliases preserve mixed-case organization qualifiers, sandbox source materializes under `/workspace/agent`, run artifact downloads use direct paths like `/files/eigenpal.lock`, wait-for-completion responses include source provenance, and CLI wait/watch commands exit nonzero for failed or cancelled runs.\n\n### SDK\n\n#### Patch Changes\n\n- c3486a5: Fix agent git cutover QA findings: dashboard runs now navigate to the created run, inbound email aliases preserve mixed-case organization qualifiers, sandbox source materializes under `/workspace/agent`, run artifact downloads use direct paths like `/files/eigenpal.lock`, wait-for-completion responses include source provenance, and CLI wait/watch commands exit nonzero for failed or cancelled runs.\n\n---\n\n## 0.6.2 (2026-06-02)\n\n### CLI\n\n#### Patch Changes\n\n- d2202b6: List all agent runs for unqualified targets, and reserve source-ref filtering for explicit `@ref` targets.\n\n---\n\n## 0.6.0 (2026-06-02)\n\n### CLI\n\n#### Minor Changes\n\n- 2aeeaf2: Add native source `git save` and branch-aware `git release` flows for Git-backed builder workspaces.\n\n#### Patch Changes\n\n- 2aeeaf2: Complete the agent Git cutover with pluralized agent CLI commands, source-ref run support, lockfile metadata, and regenerated SDK/docs surfaces.\n- 2aeeaf2: Expose Git-backed agent source refs and public source automation APIs consistently across CLI docs and SDK facades.\n\n### SDK\n\n#### Patch Changes\n\n- 2aeeaf2: Complete the agent Git cutover with pluralized agent CLI commands, source-ref run support, lockfile metadata, and regenerated SDK/docs surfaces.\n- 2aeeaf2: Expose Git-backed agent source refs and public source automation APIs consistently across CLI docs and SDK facades.\n\n---\n\n## 0.5.12 (2026-06-02)\n\n### CLI\n\n#### Minor Changes\n\n- d3b0768: Add `control.fail` and `ai.classify` step types, plus the `expected/error.json` dataset assertion format.\n - `control.fail` terminates a workflow with a typed status code + message envelope. Optional `condition` template gates the fail. Compose with `ai.classify` to reject documents that match an undesired label.\n - `ai.classify` runs single-label closed-set classification through the existing extract LLM pipeline. Output is `\\{ label, confidence, reason }`, where `label` is constrained to the configured names.\n - `examples/<name>/expected/error.json` (mutually exclusive with `output.json`) lets eval examples assert that a workflow should fail with a specific `\\{ code, messageContains, step }`. The exact-diff evaluator scores 1 on match, 0 otherwise.\n - `eigenpal workflow validate` now catches `expected/error.json` shape issues and per-step config violations (e.g. `ai.classify` with fewer than two labels) at push time instead of deferring to runtime.\n\n Skill reference docs (`step-types.md`, `dataset-format.md`, `evaluators.md`, `workflow-yaml.md`) updated.\n\n#### Patch Changes\n\n- 99cad97: Skill reference (auto-generated from `WorkflowDefinitionSchema`) now documents the new `values` row for enum inputs, alongside the existing `items` row used by array inputs. Output-only doc refresh — no CLI behavior change.\n- d70f697: Document the `control.parallel`, `control.parallel_map`, `control.foreach`, `control.if`, and `control.block` step types in the bundled skill reference. The auto-generated catalog cannot render nested step shapes, so the YAML form, output access paths (e.g. `steps.<parallel>.output.<branch>.<inner>.<field>`), and scoping rules now live as hand-written prose. Also documents the multi-step iteration hazard: only the LAST inner step's output is keyed into each `items[i]` for `parallel_map` / `foreach` — end the body with a `transform.script` if you need to preserve intermediate fields.\n- ed2cde6: Fix `eigenpal workflow pull` writing a 0-byte file. The v1 single-workflow endpoint now returns the current YAML at the top level (`yamlContent`) via a new `WorkflowDetail` response shape, and the CLI throws a clear error instead of silently writing empty output when no version has been published. Also fixes adjacent regressions caused by the same picker: `workflow list` now shows the workflow name and version columns (previously always `-`), `workflow push --bump` reads the server's current version correctly, the \"did you mean?\" suggestion on a slug typo lists candidates again, and `workflow execution list` shows a duration column computed from the run start/end timestamps.\n\n---\n\n## 0.5.11 (2026-05-26)\n\n### CLI\n\n#### Patch Changes\n\n- 2958f4d: Fix Git-backed source smoke-test polish: hidden `git show` and `git sync` now accept bare agent slugs, and workflow scaffolds use the current `triggerMethods` YAML shape.\n\n---\n\n## 0.5.10 (2026-05-26)\n\n### CLI\n\n#### Patch Changes\n\n- aee7e0c: Add hidden Git source commands for editing trigger policy and encrypted secrets.\n\n---\n\n## 0.5.9 (2026-05-26)\n\n### CLI\n\n#### Major Changes\n\n- fadb59b: CLI command tree reorganized around the data model. Top-level surface is now\n `workflow`, `agent` (reserved), and `skill`; everything that operates on a\n workflow lives under `workflow`, grouped by sub-entity.\n\n Renames (clean break — no aliases):\n - `eigenpal exec <slug>` → `eigenpal workflow execution run <slug>`\n - `eigenpal clear` → `eigenpal workflow clear`\n - `eigenpal install-skill` → `eigenpal skill` (same interactive multiselect UX)\n - `eigenpal execution \\{get,list,watch,compare}` → `eigenpal workflow execution \\{…}`\n - `eigenpal workflow set-definition` → `eigenpal workflow push`\n - `eigenpal workflow get-definition` → `eigenpal workflow pull`\n - `eigenpal workflow set-evaluators` / `get-evaluators` → `workflow evaluators \\{push,pull}`\n - `eigenpal workflow set-dataset` / `get-dataset` / `list-examples` → `workflow dataset \\{push,pull,list}`\n - `eigenpal workflow run-experiment` / `get-experiment-status` / `get-experiment-results` /\n `get-eval-results` / `list-executions` → `workflow experiment \\{run,status,results,eval-results,list}`\n - `eigenpal workflow list-versions` / `restore-version` → `workflow version \\{list,restore}`\n\n[View full release notes →](/releases/0.5.9)\n\n{/* GENERATED:CHANGELOG_INDEX END */}\n"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"topic": "cli/agents",
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
"title": "Workflow steps",
|
|
175
175
|
"description": "The complete catalog of step types, each with a code-derived config and output schema.",
|
|
176
176
|
"source": "public",
|
|
177
|
-
"content": "---\ntitle: Workflow steps\ndescription: The complete catalog of step types, each with a code-derived config and output schema.\n---\n\nEvery step type the workflow engine supports has its own reference page. The\nprose on each page is hand-written; the **Configuration** and **Output** schema\nsections are generated from `STEP_SCHEMAS` in `@eigenpal/types`, so they always\nmatch what the engine actually accepts and returns.\n\nSteps fall into four categories, see [Workflows](/concepts/workflows) for how\nthey fit together.\n\n## Full catalog\n\n{/* GENERATED:STEP_INDEX START */}\n### AI steps\n\n| Step | Type | Summary |\n| --- | --- | --- |\n| [Parse Document](/steps/ai/parse) | `ai.parse` | Extract text from documents (PDF, DOCX, images) using native extraction, OCR, or vision models |\n| [Extract Data](/steps/ai/extract) | `ai.extract` | Extract structured data from text using AI with a JSON schema |\n| [Split Document](/steps/ai/split) | `ai.split` | Split a parsed document into named sections using an LLM. Consumes ai.parse output; emits per-section page ranges and text ready for downstream ai.extract via control.parallel_map. |\n| [Separate Documents](/steps/ai/segment) | `ai.segment` | Separate a concatenated batch (one big scan) into typed document instances using an LLM. Consumes ai.parse output and a type taxonomy; discovers an unknown number of documents in any order and emits per-document page ranges + text + type, ready for type-specific ai.extract via control.parallel_map. The inverse of ai.split. |\n| [Classify](/steps/ai/classify) | `ai.classify` | Classify a document or text into one of a fixed label set using an LLM. Output exposes the picked label (constrained to the configured names), a coarse confidence, and a short justification. Pair with control.fail to reject documents that match an undesired label. |\n| [Label Pages](/steps/ai/classify-pages) | `ai.classify-pages` | Assign zero or more labels to each page independently (multi-label) using an LLM. Consumes ai.parse output; emits per-page labels and a byLabel map (label -> page indices) that supports NON-contiguous selections. Feed byLabel.<label> straight into ai.vision `pageIndices` to inspect scattered pages of a type (e.g. every signature or property-photo page). |\n| [Inspect Pages (Vision)](/steps/ai/vision) | `ai.vision` | Inspect rendered page images with a vision model and return structured JSON matching a schema. The visual counterpart to Extract Data: use it for conclusions that live in the pixels rather than the text (is the document signed? are the photos usable?). Renders PDF, image, or Office/Word inputs; route to specific pages with an ai.split page range to keep it cheap. |\n| [Search Files](/steps/ai/search-files) | `ai.search-files` | Investigate a ZIP archive with a question and return an answer plus file citations. Read-only: the step opens files in the archive and never writes back. ZIP is the only accepted format in this version. Optionally scope the search to a folder prefix and cap how many files and investigation turns may run. |\n| [Figure Crops](/steps/ai/figure-crops) | `ai.figure-crops` | Crop and caption figure elements from a parsed PDF. Renders the source document, crops each kind=figure bbox from ai.parse output, and describes the crop with a vision model. Output is captions plus normalized bboxes for evidence viewers. |\n\n### Transform steps\n\n| Step | Type | Summary |\n| --- | --- | --- |\n| [Set Value](/steps/transform/set) | `transform.set` | Set key-value pairs in the output object |\n| [Remove Fields](/steps/transform/remove) | `transform.remove` | Remove specified fields from an object |\n| [Combine Data](/steps/transform/combine) | `transform.combine` | Merge multiple objects or concatenate arrays |\n| [Split Data](/steps/transform/split) | `transform.split` | Split a string by delimiter or extract keys from an object |\n| [Merge Inputs](/steps/transform/merge) | `transform.merge` | Merge multiple named inputs into a single output |\n| [Fill Template](/steps/transform/template) | `transform.template` | Fill a DOCX or XLSX workspace template with data from previous steps. Reference a tmpl_... id, or a local ./templates/file.xlsx path (CLI push uploads it). Git agent templates use the fill-template skill instead and cannot be referenced here. File IDs (file_...) are not accepted. Placeholders are auto-detected. In the Office file use {placeholder} and {table:array.prop}; {{placeholder}} in an XLSX file is rejected ({{ }} is YAML Liquid only). |\n| [Embed PDF Text](/steps/transform/pdf-embed) | `transform.pdf-embed` | Embed OCR text layer into scanned PDFs/images to make them searchable |\n| [Spreadsheet to JSON](/steps/transform/xlsx-to-json) | `transform.xlsx-to-json` | Convert an XLS or XLSX spreadsheet to a JSON array of row objects. Supports headerless files, named or positional columns, displayed text, and a single rectangular range. |\n| [JSON to XLSX](/steps/transform/json-to-xlsx) | `transform.json-to-xlsx` | Convert JSON rows into an XLSX spreadsheet. Pair with transform.xlsx-to-json. Formula-looking strings stay text; nested cell values are rejected. |\n| [Script](/steps/transform/script) | `transform.script` | Execute a TypeScript function in a QuickJS sandbox. Input keys become function parameters in any order, and the required `: R` return-type annotation IS this step's output schema: `inputs: { items, taxRate }` accepts `function script(taxRate: …, items: …): R { … }`. |\n| [Text Chunker](/steps/transform/text-chunker) | `transform.text-chunker` | Split long text into chunks with regex-anchored boundaries, overlap, and header preservation. Accepts raw text or a parsed-document object; chunks carry source page indexes when pages are provided. |\n| [Regex Extract](/steps/transform/regex-extract) | `transform.regex-extract` | Pull named fields from text via regex patterns (deterministic counterpart to ai.extract). Accepts raw text or a parsed-document object; matches carry `_evidence.pageIndex` when pages are provided. |\n| [List Archive](/steps/transform/archive-list) | `transform.archive-list` | List files inside a ZIP archive. Returns sorted normalized paths with compressed and uncompressed sizes. Optionally filter by folder prefix or substring, then page with offset and limit. ZIP is the only accepted format in this version. Pair with foreach or parallel_map and transform.archive-extract to open individual files. |\n| [Extract Archive File](/steps/transform/archive-extract) | `transform.archive-extract` | Extract exactly one file from a ZIP archive and store it as a run output file. Downstream steps can pass {{ steps.extract.output }} into Parse, Vision, or another archive step. Nested ZIPs are stored as ZIP files, not unpacked. Each extraction is capped at 64 MiB. ZIP is the only accepted format in this version. |\n\n### Action steps\n\n| Step | Type | Summary |\n| --- | --- | --- |\n| [HTTP Request](/steps/action/http) | `action.http` | Make an HTTP request to an external API |\n| [Invoke Workflow](/steps/action/invoke-workflow) | `action.invoke-workflow` | Execute another workflow and return its output |\n| [Website Reader](/steps/action/website-reader) | `action.website-reader` | Fetch a webpage and convert content to markdown |\n| [Send Email](/steps/action/email) | `action.email` | Send an email with optional attachments via a selected email server. At most 10 recipients combined across to, cc, and bcc. |\n\n### Control steps\n\n| Step | Type | Summary |\n| --- | --- | --- |\n| [Condition](/steps/control/if) | `control.if` | Branch execution based on a condition expression |\n| [Switch](/steps/control/switch) | `control.switch` | Multi-way routing: resolve an expression and run the first case whose value matches (else default). Cleaner than a nested control.if chain for routing an item to one of N pipelines by a discriminator field like a document type. |\n| [For Each](/steps/control/foreach) | `control.foreach` | Loop over an array and execute steps for each item |\n| [Parallel Map](/steps/control/parallel_map) | `control.parallel_map` | Iterate over an array with concurrent execution up to a limit |\n| [Parallel](/steps/control/parallel) | `control.parallel` | Execute multiple branches concurrently |\n| [Wait](/steps/control/wait) | `control.wait` | Pause workflow execution for a specified duration |\n| [Human Review](/steps/control/human_review) | `control.human_review` | Pause the run for selective field confirmation or correction, then continue with the reviewed data. |\n| [Fail](/steps/control/fail) | `control.fail` | Terminate the workflow with a typed status code + message. With an optional condition, only fails when the condition is truthy; otherwise always fails when reached. Pair with ai.classify or any prior step to fail fast on bad inputs. |\n{/* GENERATED:STEP_INDEX END */}\n"
|
|
177
|
+
"content": "---\ntitle: Workflow steps\ndescription: The complete catalog of step types, each with a code-derived config and output schema.\n---\n\nEvery step type the workflow engine supports has its own reference page. The\nprose on each page is hand-written; the **Configuration** and **Output** schema\nsections are generated from `STEP_SCHEMAS` in `@eigenpal/types`, so they always\nmatch what the engine actually accepts and returns.\n\nSteps fall into four categories, see [Workflows](/concepts/workflows) for how\nthey fit together.\n\n## Full catalog\n\n{/* GENERATED:STEP_INDEX START */}\n### AI steps\n\n| Step | Type | Summary |\n| --- | --- | --- |\n| [Parse Document](/steps/ai/parse) | `ai.parse` | Extract text from documents (PDF, DOCX, images) using native extraction, OCR, or vision models |\n| [Extract Data](/steps/ai/extract) | `ai.extract` | Extract structured data from text using AI with a JSON schema |\n| [Split Document](/steps/ai/split) | `ai.split` | Split a parsed document into named sections using an LLM. Consumes ai.parse output; emits per-section page ranges and text ready for downstream ai.extract via control.parallel_map. |\n| [Separate Documents](/steps/ai/segment) | `ai.segment` | Separate a concatenated batch (one big scan) into typed document instances using an LLM. Consumes ai.parse output and a type taxonomy; discovers an unknown number of documents in any order and emits per-document page ranges + text + type, ready for type-specific ai.extract via control.parallel_map. The inverse of ai.split. |\n| [Classify](/steps/ai/classify) | `ai.classify` | Classify a document or text into one of a fixed label set using an LLM. Output exposes the picked label (constrained to the configured names), a coarse confidence, and a short justification. Pair with control.fail to reject documents that match an undesired label. |\n| [Label Pages](/steps/ai/classify-pages) | `ai.classify-pages` | Assign zero or more labels to each page independently (multi-label) using an LLM. Consumes ai.parse output; emits per-page labels and a byLabel map (label -> page indices) that supports NON-contiguous selections. Feed byLabel.<label> straight into ai.vision `pageIndices` to inspect scattered pages of a type (e.g. every signature or property-photo page). |\n| [Inspect Pages (Vision)](/steps/ai/vision) | `ai.vision` | Inspect rendered page images with a vision model and return structured JSON matching a schema. The visual counterpart to Extract Data: use it for conclusions that live in the pixels rather than the text (is the document signed? are the photos usable?). Renders PDF, image, or Office/Word inputs; route to specific pages with an ai.split page range to keep it cheap. |\n| [Search Files](/steps/ai/search-files) | `ai.search-files` | Investigate a ZIP archive with a question and return an answer plus file citations. Read-only: the step opens files in the archive and never writes back. ZIP is the only accepted format in this version. Optionally scope the search to a folder prefix and cap how many files and investigation turns may run. |\n| [Figure Crops](/steps/ai/figure-crops) | `ai.figure-crops` | Crop and caption figure elements from a parsed PDF. Renders the source document, crops each kind=figure bbox from ai.parse output, and describes the crop with a vision model. Output is captions plus normalized bboxes for evidence viewers. |\n\n### Transform steps\n\n| Step | Type | Summary |\n| --- | --- | --- |\n| [Set Value](/steps/transform/set) | `transform.set` | Set key-value pairs in the output object |\n| [Remove Fields](/steps/transform/remove) | `transform.remove` | Remove specified fields from an object |\n| [Combine Data](/steps/transform/combine) | `transform.combine` | Merge multiple objects or concatenate arrays |\n| [Split Data](/steps/transform/split) | `transform.split` | Split a string by delimiter or extract keys from an object |\n| [Merge Inputs](/steps/transform/merge) | `transform.merge` | Merge multiple named inputs into a single output |\n| [Fill Template](/steps/transform/template) | `transform.template` | Fill a DOCX or XLSX workspace template with data from previous steps. Reference a tmpl_... id, or a local ./templates/file.xlsx path (CLI push uploads it). Git agent templates use the fill-template skill instead and cannot be referenced here. File IDs (file_...) are not accepted. Placeholders are auto-detected. In the Office file use {placeholder} and {table:array.prop}; {{placeholder}} in an XLSX file is rejected ({{ }} is YAML Liquid only). |\n| [Embed PDF Text](/steps/transform/pdf-embed) | `transform.pdf-embed` | Embed OCR text layer into scanned PDFs/images to make them searchable |\n| [Crop Regions](/steps/transform/crop-regions) | `transform.crop-regions` | Render a PDF or image and crop normalized bounding boxes into JPEG run output artifacts. Pair with transform.script (bbox lists from ai.parse layout) and ai.extract or ai.vision for captions. |\n| [Spreadsheet to JSON](/steps/transform/xlsx-to-json) | `transform.xlsx-to-json` | Convert an XLS or XLSX spreadsheet to a JSON array of row objects. Supports headerless files, named or positional columns, displayed text, and a single rectangular range. |\n| [JSON to XLSX](/steps/transform/json-to-xlsx) | `transform.json-to-xlsx` | Convert JSON rows into an XLSX spreadsheet. Pair with transform.xlsx-to-json. Formula-looking strings stay text; nested cell values are rejected. |\n| [Script](/steps/transform/script) | `transform.script` | Execute a TypeScript function in a QuickJS sandbox. Input keys become function parameters in any order, and the required `: R` return-type annotation IS this step's output schema: `inputs: { items, taxRate }` accepts `function script(taxRate: …, items: …): R { … }`. |\n| [Text Chunker](/steps/transform/text-chunker) | `transform.text-chunker` | Split long text into chunks with regex-anchored boundaries, overlap, and header preservation. Accepts raw text or a parsed-document object; chunks carry source page indexes when pages are provided. |\n| [Regex Extract](/steps/transform/regex-extract) | `transform.regex-extract` | Pull named fields from text via regex patterns (deterministic counterpart to ai.extract). Accepts raw text or a parsed-document object; matches carry `_evidence.pageIndex` when pages are provided. |\n| [List Archive](/steps/transform/archive-list) | `transform.archive-list` | List files inside a ZIP archive. Returns sorted normalized paths with compressed and uncompressed sizes. Optionally filter by folder prefix or substring, then page with offset and limit. ZIP is the only accepted format in this version. Pair with foreach or parallel_map and transform.archive-extract to open individual files. |\n| [Extract Archive File](/steps/transform/archive-extract) | `transform.archive-extract` | Extract exactly one file from a ZIP archive and store it as a run output file. Downstream steps can pass {{ steps.extract.output }} into Parse, Vision, or another archive step. Nested ZIPs are stored as ZIP files, not unpacked. Each extraction is capped at 64 MiB. ZIP is the only accepted format in this version. |\n\n### Action steps\n\n| Step | Type | Summary |\n| --- | --- | --- |\n| [HTTP Request](/steps/action/http) | `action.http` | Make an HTTP request to an external API |\n| [Invoke Workflow](/steps/action/invoke-workflow) | `action.invoke-workflow` | Execute another workflow and return its output |\n| [Website Reader](/steps/action/website-reader) | `action.website-reader` | Fetch a webpage and convert content to markdown |\n| [Send Email](/steps/action/email) | `action.email` | Send an email with optional attachments via a selected email server. At most 10 recipients combined across to, cc, and bcc. |\n\n### Control steps\n\n| Step | Type | Summary |\n| --- | --- | --- |\n| [Condition](/steps/control/if) | `control.if` | Branch execution based on a condition expression |\n| [Switch](/steps/control/switch) | `control.switch` | Multi-way routing: resolve an expression and run the first case whose value matches (else default). Cleaner than a nested control.if chain for routing an item to one of N pipelines by a discriminator field like a document type. |\n| [For Each](/steps/control/foreach) | `control.foreach` | Loop over an array and execute steps for each item |\n| [Parallel Map](/steps/control/parallel_map) | `control.parallel_map` | Iterate over an array with concurrent execution up to a limit |\n| [Parallel](/steps/control/parallel) | `control.parallel` | Execute multiple branches concurrently |\n| [Wait](/steps/control/wait) | `control.wait` | Pause workflow execution for a specified duration |\n| [Human Review](/steps/control/human_review) | `control.human_review` | Pause the run for selective field confirmation or correction, then continue with the reviewed data. |\n| [Fail](/steps/control/fail) | `control.fail` | Terminate the workflow with a typed status code + message. With an optional condition, only fails when the condition is truthy; otherwise always fails when reached. Pair with ai.classify or any prior step to fail fast on bad inputs. |\n{/* GENERATED:STEP_INDEX END */}\n"
|
|
178
178
|
},
|
|
179
179
|
{
|
|
180
180
|
"topic": "concepts/why",
|
|
@@ -419,14 +419,14 @@
|
|
|
419
419
|
"title": "Step types",
|
|
420
420
|
"description": "Detailed agent reference bundled with the Eigenpal CLI.",
|
|
421
421
|
"source": "agent-reference",
|
|
422
|
-
"content": "# Step types\n\nThe catalog is generated from `STEP_SCHEMAS` in\n`@eigenpal/types/src/workflow/step-configs.ts`. **Do not memorize fields\n— introspect.** The CLI reads the live schema:\n\n```bash\n# List every step type with a one-line description\neigenpal workflow step-type list\n\n# Filter by keyword\neigenpal workflow step-type list --search extract\n\n# Full JSON Schema for one type's `config` + `output` + behavioral docs\neigenpal workflow step-type get ai.extract\n```\n\nThe output of `step-type get` is the **authoritative source** for which\nfields each step takes, what shapes they accept, and what behavior they\nimplement at runtime. Server-side validation errors point you here.\n\n## Categories (high-level map)\n\n- `ai.*` — model-backed processing (parse, extract, classify). Cost + latency depend on the model.\n- `transform.*` — deterministic data transforms (set, remove, combine, split, merge,\n script, template, pdf-embed, xlsx-to-json, json-to-xlsx). WASM sandboxed where applicable.\n- `action.*` — external side effects (HTTP, invoke another workflow, website reader).\n- `control.*` — flow control (if, foreach, parallel, parallel_map, wait, human_review, fail).\n\nThe full per-type catalog with field tables is auto-generated below from\n`STEP_SCHEMAS`. The high-level map above tells you which family you want;\nthe catalog tells you what fields it takes.\n\n## When to reach for what\n\n| Use case | Step |\n| --------------------------------- | ------------------------------------------------- |\n| Read a PDF / DOCX / image (native-first) | `ai.parse` with `parseMode: native` or `native-or-ocr` |\n| Read a PDF / DOCX / image (full OCR/vision) | `ai.parse` with `parseMode: ocr` or `vision` |\n| Pull a typed object from text | `ai.extract` with `config.schema` |\n| Pick one label from a fixed set | `ai.classify` with `config.labels` |\n| Reject bad inputs with a 4xx code | `control.fail` (often after `ai.classify`) |\n| Pause for human field review | `control.human_review` (top-level only; see below) |\n| Sum / filter / regex | `transform.script` (NOT Liquid) |\n| Render a DOCX or XLSX template (YAML workflow) | `transform.template` with `tmpl_...` or `./file.xlsx` |\n| Fill a DOCX or XLSX template (runtime agent) | fill-template platform skill |\n| Convert XLSX to JSON | `transform.xlsx-to-json` |\n| Convert JSON rows to XLSX | `transform.json-to-xlsx` |\n| Merge or combine objects | `transform.combine` / `transform.merge` |\n| Conditional execution | `if:` on the step (Liquid) or `control.if` |\n| Map over an input array | `forEach:` on the step or `control.foreach` |\n| Concurrent map over an array | `control.parallel_map` |\n| Independent parallel branches | `control.parallel` |\n| External HTTP call | `action.http` |\n| Call another workflow | `action.invoke-workflow` |\n| Fetch a webpage as markdown | `action.website-reader` |\n\n## Workspace vs Git templates\n\nTwo separate template systems share DOCX/XLSX placeholder syntax but not\nreferences:\n\n| Goal | Mechanism |\n| --- | --- |\n| YAML workflow produces a filled DOCX/XLSX | `transform.template` with `templateId: tmpl_...` or `template: ./templates/foo.xlsx` (CLI push uploads) |\n| Runtime agent produces a filled DOCX/XLSX | fill-template platform skill against Git templates |\n\nGit templates (`agents/<agent>/templates/<slug>/`, shared\n`resources/templates/<slug>/`) never receive a `tmpl_...` ID and cannot be\nused in `transform.template`. Manage Git templates through the agent source\nloop — see the eigenpal skill's **Agent templates (Git source)** section.\n\nWorkspace templates are tenant-scoped `tmpl_...` resources with immutable\n`tmpr_...` revisions. Manage them from the CLI (preferred) or Studio:\n\n```bash\neigenpal workflow templates upload ./templates/roster.xlsx --json\neigenpal workflow templates list --json\neigenpal workflow templates get tmpl_... --json\neigenpal workflow templates smoke ./templates/roster.xlsx --data ./fixture.json --out ./filled.xlsx\neigenpal workflow validate ./workflow.yaml --online\neigenpal workflow push --file workflow.yaml\n```\n\nAuthor YAML with either a live id or a source-controlled path. Push uploads\nthe file (skipped when the SHA-256 still matches) and sends `templateId` +\ncurrent `templateRevisionId` to the server without rewriting the file on disk.\n\n`templateId` must be `tmpl_...` when not using a local path; file IDs\n(`file_...`) are rejected at validate/push.\n\nIn the Office file itself use `{placeholder}` and, for XLSX row expansion,\n`{table:array.prop}`. `{{placeholder}}` is Liquid for YAML `data` mapping\nonly — putting it in an XLSX file fails at fill. Example prototype row:\n\n```yaml\n- name: fill-roster\n type: transform.template\n with:\n templateId: tmpl_...\n data:\n report_title: \"{{ steps.extract.output.title }}\"\n subjects: \"{{ steps.extract.output.subjects }}\"\n```\n\nSpreadsheet cells: `{report_title}`, `{table:subjects.first_name}`,\n`{table:subjects.last_name}`. Null/blank cells stay empty; numbers and\nbooleans keep spreadsheet types; every sheet is filled; existing formulas\nstay formulas.\n\n## Liquid vs `transform.script`\n\n| Logic shape | Use |\n| -------------------------- | -------------------------------- |\n| `steps.x.output.field` | Liquid `{{ ... }}` |\n| `value \\| default: 0` | Liquid |\n| `price \\| times: quantity` | Liquid |\n| array `reduce` / `filter` | `transform.script` |\n| multi-step calc | `transform.script` |\n| conditional branches | `transform.script` or `if:` |\n| date arithmetic | `transform.script` |\n| regex | `transform.script` |\n\n`transform.script` advantages over Liquid:\n\n- Explicit `inputs:` declaration; the validator knows the data deps\n- Typed return annotation drives downstream autocomplete + runtime validation\n- Real JS/TS (`Array.prototype.reduce`, `JSON.parse`, etc.)\n- Sandbox limits prevent infinite loops from blowing up an execution\n\n> The function is **TypeScript**. Each `inputs:` key becomes a parameter\n> in declaration order: `inputs: { items, taxRate }` ⇒\n> `function script(items: ..., taxRate: ...): R { ... }`. The return-type\n> annotation `R` is **required** and IS this step's output schema; there\n> is no separate `outputSchema:` field. Describe what your function\n> actually returns: downstream steps reference its fields via\n> `{{ steps.<this>.output.<field> }}`, so a too-loose annotation (e.g.\n> `unknown`) makes those references unresolvable. The function must\n> `return` (or `throw`) a value.\n\n## Reference / output paths\n\nAfter a step runs, its output is reachable via\n`{{ steps.<name>.output.<field> }}`. The exact field set per type comes\nfrom `step-type get`'s `outputSchema`. Example for `ai.extract`:\n\n```bash\n$ eigenpal workflow step-type get ai.extract | jq '.outputSchema'\n{\n \"type\": \"object\",\n \"properties\": {\n \"extracted\": { /* matches the user-supplied config.schema */ },\n \"rawResponse\": { \"type\": \"string\" },\n \"tokensUsed\": { \"type\": \"number\" }\n }\n}\n```\n\nReference user-supplied schema fields the same way:\n`{{ steps.extract.output.extracted.invoiceNumber }}`.\n\n## Human review — top-level gate only\n\n`control.human_review` pauses the run until a reviewer confirms or corrects\nselected scalar fields, then resumes with the reviewed object or array. Place\nit as a **top-level sequential step** only — nested placement inside\n`control.if`, `control.foreach`, `control.parallel`, `control.parallel_map`, or\n`control.switch` is rejected at publish time.\n\nConfig lives in `step.with` (unlike most other `control.*` steps). Wire data\nfrom an upstream extract or transform, choose a selection mode, and attach run\nfiles for context:\n\n```yaml\n- name: review-invoice\n type: control.human_review\n with:\n data: '{{ steps.extract.output }}'\n metadataFrom: ai.extract\n instructions: Confirm vendor and total before posting.\n selection:\n mode: confidence\n threshold: medium\n missingConfidence: review\n fields:\n /vendor:\n review: always\n /total:\n threshold: high\n fieldMetadata:\n /vendor:\n label: Vendor name\n /total:\n label: Invoice total\n files:\n includeRunInputs: true\n attachments:\n - '{{ steps.render.output.files[0] }}'\n```\n\nWhen `metadataFrom: ai.extract` is set and `data` is the whole extract output\n(`{{ steps.<name>.output }}`), the step strips the reserved `_grounding` key,\nderives categorical confidence (`low`, `medium`, `high`) per field, and reuses\nthe extract schema when `schema` is omitted.\n\n**Selection:** three modes — `confidence` (review fields below threshold),\n`explicit` (review matched include/exclude paths), and `all` (every scalar\nleaf). Thresholds accept numeric `0`–`1` or categorical `low|medium|high`;\nequality auto-approves. Per-path overrides live under `selection.fields` with\n`review: always|never|skip` and optional per-field `threshold`. Prefer\n`selection.fields` over the legacy `fieldMetadata.review`.\n\n**Output:** the complete reviewed data in the same shape as `data`. Reference\ndownstream fields directly: `{{ steps.review-invoice.output.vendor }}`.\n\n**Evaluations:** evaluation runs always simulate review and never pause the\nrun. Use `meta.json` `review` fixtures to assert routing, reasons, and\nsimulated reviewer edits — see `reference/dataset-format.md`. Empty selection\ncontinues immediately without creating a task.\n\n**Limits:** 2 MiB review JSON, 10k scalar leaves, 1 MiB metadata, 2k required\nfields, 100 attachments.\n\nUse `eigenpal workflow step-type get control.human_review` for the full schema.\n\n## Control containers — nested step shape and scoping\n\n`control.parallel`, `control.parallel_map`, `control.foreach`, `control.if`,\nand inline `action.invoke-workflow` (`execution: inline`) contain nested steps. The auto-generated catalog below\ncannot render that shape, so the YAML form lives here. Same for the scoping\nrules — important because the runtime treats nested steps differently from\ntop-level ones, and getting the access path wrong silently returns\n`undefined` (no error).\n\n### `control.parallel` — independent branches\n\nRuns each branch concurrently. Use when several pieces of work share input\nbut do not depend on each other.\n\n```yaml\n- name: enrich\n type: control.parallel\n branches:\n - name: legal\n steps:\n - name: extract-clauses\n type: ai.extract\n with:\n input: '{{ steps.parse.output.markdown }}'\n schema: { clauses: { type: array, items: { type: string } } }\n - name: financial\n steps:\n - name: extract-totals\n type: ai.extract\n with:\n input: '{{ steps.parse.output.markdown }}'\n schema: { total: { type: number } }\n```\n\n**Access the output:** branch step outputs are stored under the parent\nparallel step keyed by branch name, then by inner step name. There is no\nflat top-level entry for branch-internal steps.\n\n```liquid\n{{ steps.enrich.output.legal.extract-clauses.clauses }}\n{{ steps.enrich.output.financial.extract-totals.total }}\n```\n\n**Scope inside a branch:** top-level steps that ran before the parallel,\nplus earlier siblings in the same branch. **Other branches' steps are\nNOT visible** — each branch runs in an isolated child scope.\n\n```liquid\n# Inside branch `legal`, second step:\n{{ steps.parse.output.markdown }} # ancestor: visible\n{{ steps.extract-clauses.output.clauses }} # sibling: visible\n{{ steps.extract-totals.output.total }} # other branch: UNDEFINED\n```\n\n### `control.parallel_map` — fan-out over an array\n\nRuns the inner step block concurrently for each item, up to `concurrency`.\n\n```yaml\n- name: process-pages\n type: control.parallel_map\n items: '{{ steps.split.output.pages }}'\n as: page\n indexAs: i # optional\n concurrency: 5 # default 5, max 50\n steps:\n - name: extract-fields\n type: ai.extract\n with:\n input: '{{ page.text }}'\n schema: { fields: { type: object } }\n```\n\n**Access the output:** iteration results are returned as an array in the\noriginal input order under the parent step's `items` field.\n\n```liquid\n{{ steps.process-pages.output.items[0].extract-fields.fields }}\n{{ steps.process-pages.output.count }} # number of completed iterations\n{{ steps.process-pages.output.totalIterations }}\n```\n\n`steps.extract-fields.output.fields` (the flat form) is **not** addressable\nfrom outside the parallel_map.\n\n**Multi-step iterations only key the LAST step's output into `items[i]`.**\nIf your iteration body has steps `[parse, extract]`, each `items[i]` is\n`{ extract: <extract output> }` — `parse`'s output is gone. To preserve\nintermediate fields, end the body with a `transform.script` that returns\nthe combined shape:\n\n```yaml\nsteps:\n - name: parse\n type: ai.parse\n with: { ... }\n - name: extract\n type: ai.extract\n with: { input: '{{ steps.parse.output.markdown }}', ... }\n - name: combine\n type: transform.script\n with:\n inputs:\n parsed: '{{ steps.parse.output }}'\n extracted: '{{ steps.extract.output }}'\n function: |\n function script(parsed, extracted): { parsed: any; extracted: any } {\n return { parsed, extracted };\n }\n```\n\nNow `items[i].combine.parsed` / `items[i].combine.extracted` both survive.\n\n### `control.foreach` — sequential loop over an array\n\nSame shape as `parallel_map` minus `concurrency`; iterations run in order.\nUse when each iteration depends on the previous, or when concurrency would\ncause downstream rate limits.\n\n```yaml\n- name: per-row\n type: control.foreach\n items: '{{ steps.fetch.output.rows }}'\n as: row\n steps:\n - name: persist\n type: action.http-request\n with:\n method: POST\n url: 'https://example.com/api/rows/{{ row.id }}'\n body: '{{ row }}'\n```\n\n**Access the output:** same shape as parallel_map — an `items[]` array of\nper-iteration results, in iteration order. Same \"only the LAST inner step\nis keyed into `items[i]`\" rule applies (use a trailing `transform.script`\nto preserve intermediate outputs).\n\n> ⚠️ Foreach does NOT isolate scopes — the last iteration's inner step\n> outputs technically persist on the top-level `steps` map and `steps.<inner>.output.x`\n> resolves at runtime. That is an only-the-last-row hazard; the autocomplete\n> hides these names and you should treat `steps.<foreach>.output.items[]`\n> as the only correct access.\n\n### `control.if` — conditional branch\n\n```yaml\n- name: route\n type: control.if\n condition: '{{ input.priority == \"high\" }}'\n then:\n - name: rush-extract\n type: ai.extract\n with: { ... }\n else:\n - name: normal-extract\n type: ai.extract\n with: { ... }\n```\n\n**Access the output:** the `if` step's output is\n\n```ts\n{\n condition: boolean, // evaluated condition\n branch: 'then' | 'else', // which branch ran\n result: <last inner step's output> | null // null when chosen branch is empty\n}\n```\n\nThe canonical way to consume the result is through the if step itself:\n\n```liquid\n{{ steps.route.output.branch }} # \"then\" or \"else\"\n{{ steps.route.output.result.foo }} # field from the branch's last step\n```\n\n`steps.<innerName>.output` references from outside the `if` also resolve\nat runtime (unlike `parallel`/`parallel_map`, `if` does not isolate\nscopes), but the template breaks the moment the OTHER branch runs. Use\n`steps.<if>.output.result` so the access is branch-agnostic — or compute\nthe final shape in a trailing `transform.script` that handles both cases.\n\n### `action.invoke-workflow` (inline) — call another workflow in the same run\n\n```yaml\n- name: invoice-flow\n type: action.invoke-workflow\n with:\n workflow: parse-invoice # target workflow name or wf_ id\n execution: inline # default; omit for same-run execution\n input:\n contract: '{{ input.contract }}'\n```\n\n**Access the output:** the target's declared output fields are exposed as\ntop-level keys under `steps.<this-step>.output`, with a `files` array alongside.\nInvoked workflow steps run in an isolated scope inside the parent run.\n\nFor a separate child run (lineage, fire-and-forget, polling), set `execution: child`.\n\n## Classify-and-fail pattern\n\n`ai.classify` + `control.fail` together let a workflow reject bad inputs\nfast with a typed HTTP-style status code that callers (and evals) can\nmatch against. The classifier picks one label from a closed set; the\nfail step terminates the run when the label is one you do not want to\nprocess.\n\n```yaml\n- name: parse\n type: ai.parse\n with:\n input: '{{ input.document }}'\n\n- name: classify\n type: ai.classify\n with:\n input: '{{ steps.parse.output.text }}'\n labels:\n - name: invoice\n description: A vendor invoice with line items and totals.\n - name: contract\n description: A multi-page contract or agreement.\n - name: other\n description: Anything that is not an invoice or contract.\n\n- name: reject-unsupported\n type: control.fail\n condition: '{{ steps.classify.output.label == \"other\" }}'\n statusCode: 422\n message: 'Unsupported document: {{ steps.classify.output.reason }}'\n```\n\n> `control.fail` config is step-level (no `with:`), matching every other\n> `control.*` step. `condition`, `statusCode`, and `message` sit directly\n> on the step node.\n\nThe synchronous run endpoint surfaces the `statusCode` as the HTTP\nresponse status; async runs persist `{ code, message, step }` to\n`executions.error` so the eval scorer can match against\n`expected.json` with `$error` (see `reference/dataset-format.md`). When\n`condition` is omitted, `control.fail` always fails when reached, so\ncompose with `control.if` for legacy gating.\n\n<!-- GENERATED:STEP_CATALOG START -->\n## Full catalog\n\n_Generated from `STEP_SCHEMAS` in `@eigenpal/types/src/workflow/step-configs.ts`. Do not hand-edit between the GENERATED fences — run `bun run --cwd packages/cli generate:skill`._\n\n### AI steps — model-backed processing\n\n#### `ai.parse` — Parse Document\n\nExtract text from documents (PDF, DOCX, images) using native extraction, OCR, or vision models\n\n**Behavior and examples:** `eigenpal docs read steps/ai/parse`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | Storage reference or template expression for the document |\n| `parseMode` | `\"ocr\"` \\| `\"vision\"` \\| `\"native\"` \\| `\"native-or-ocr\"` | no | | Base parser for PDF/image inputs. OCR is the default; vision uses the selected LLM. `native` extracts PDF embedded text only and never calls OCR or vision. `native-or-ocr` extracts native text, runs page-quality diagnostics, and requests OCR when pages have detectable anomalies (empty, U+FFFD, lone surrogates, forbidden controls, unassigned/noncharacter, heavy PUA). Page selection, subset egress, and billing are provider-dependent. Valid-looking wrong text and literal \"?\" are not flagged. Text and Office files always use local parsing. |\n| `ocrModel` | string | no | | OCR provider ID for PDF/image parsing |\n| `llmModel` | string | no | | LLM provider ID for vision-based parsing |\n| `llmReasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `figureModel` | string | no | | Vision model used only for the optional figure-description pass |\n| `figureReasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `maxConcurrency` | number | no | `3` | Max concurrent VLM batch requests |\n| `pagesPerBatch` | number | no | `5` | Number of page images per VLM request |\n| `pdfRenderScale` | number | no | `1` | Scale factor for rendering PDF pages before VLM parsing. Higher values produce sharper images at larger payload sizes. |\n| `imageQuality` | integer | no | `85` | JPEG quality for rendered PDF page images sent to VLM parsing. Higher values reduce compression artifacts at larger payload sizes. |\n| `prompt` | string | no | | Custom extraction prompt |\n| `languages` | array<string> | no | | OCR language hints |\n| `outputFormat` | `\"plain\"` \\| `\"markdown\"` \\| `\"djot\"` \\| `\"html\"` \\| `\"layout\"` | no | `\"markdown\"` | Format for extracted text. `markdown` (default) keeps structure; `plain` is unstyled text; `djot`/`html` preserve more markup. `layout` is native-PDF spatial text with column gaps preserved as spaces. It requires `parseMode: native` or `parseMode: native-or-ocr` and a PDF — omitted parseMode and `nativeText: true` are not enough. Native mode is layout-only and fail-closed. `native-or-ocr` keeps layout text on accepted native pages and uses OCR markdown on suspect or empty pages, so the document can mix fixed-width layout and markdown. Extractor failure never falls back to another parser. Office, plaintext, and images fail. OCR/vision cannot request `layout`. |\n| `nativeText` | boolean | no | `false` | Extract native/embedded text from PDFs without OCR/VLM. Faster and uses no credits. Falls back to OCR/VLM if the PDF has no embedded text. |\n| `describeFigures` | boolean | no | | Opt-in (default off). After text extraction, detect which pages contain figures with an in-worker layout model, then caption those pages with a vision model and append `<figure>description</figure>` to their text — so image-only pages (property photos, signatures, charts) become findable by text-based steps like ai.split. Note: the layout scan runs over all pages, and the caption step and its vision calls are billed. Skipped for plaintext. |\n| `figureInstructions` | string | no | | Custom instruction for the figure-description pass, e.g. \"Describe each figure; label a handwritten signature as `<figure>signature</figure>` and a stamp as `<figure>stamp</figure>`; for property photos note the room or exterior shown.\" Applied only when describeFigures runs. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `document` | object | yes | | |\n| `usage` | object | yes | | |\n| `pages` | array<object> | yes | | |\n| `text` | string | yes | | Combined text from all pages |\n| `parserType` | `\"plaintext\"` \\| `\"office\"` \\| `\"llm-vision\"` \\| `\"ocr\"` | yes | | |\n| `parserVersion` | string | no | | |\n| `model` | string | no | | Model used (for LLM/OCR parsers) |\n| `processingStrategy` | `\"native\"` \\| `\"ocr\"` \\| `\"vision\"` \\| `\"hybrid\"` | no | | How this result was produced: `native` (local PDF text only), `ocr`, `vision`, or `hybrid` (native pages merged with OCR on fallback pages). |\n| `structured` | record<string, unknown> | no | | Canonical structured document with ordered blocks, regions, bounding boxes, tables, figures, and chunks when supported by the parser |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"Storage reference or template expression for the document\"\n },\n \"parseMode\": {\n \"description\": \"Base parser for PDF/image inputs. OCR is the default; vision uses the selected LLM. `native` extracts PDF embedded text only and never calls OCR or vision. `native-or-ocr` extracts native text, runs page-quality diagnostics, and requests OCR when pages have detectable anomalies (empty, U+FFFD, lone surrogates, forbidden controls, unassigned/noncharacter, heavy PUA). Page selection, subset egress, and billing are provider-dependent. Valid-looking wrong text and literal \\\"?\\\" are not flagged. Text and Office files always use local parsing.\",\n \"type\": \"string\",\n \"enum\": [\n \"ocr\",\n \"vision\",\n \"native\",\n \"native-or-ocr\"\n ]\n },\n \"ocrModel\": {\n \"description\": \"OCR provider ID for PDF/image parsing\",\n \"type\": \"string\"\n },\n \"llmModel\": {\n \"description\": \"LLM provider ID for vision-based parsing\",\n \"type\": \"string\"\n },\n \"llmReasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"figureModel\": {\n \"description\": \"Vision model used only for the optional figure-description pass\",\n \"type\": \"string\"\n },\n \"figureReasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"maxConcurrency\": {\n \"description\": \"Max concurrent VLM batch requests\",\n \"default\": 3,\n \"type\": \"number\",\n \"minimum\": 1,\n \"maximum\": 10\n },\n \"pagesPerBatch\": {\n \"description\": \"Number of page images per VLM request\",\n \"default\": 5,\n \"type\": \"number\",\n \"minimum\": 1,\n \"maximum\": 20\n },\n \"pdfRenderScale\": {\n \"description\": \"Scale factor for rendering PDF pages before VLM parsing. Higher values produce sharper images at larger payload sizes.\",\n \"default\": 1,\n \"type\": \"number\",\n \"minimum\": 1,\n \"maximum\": 4\n },\n \"imageQuality\": {\n \"description\": \"JPEG quality for rendered PDF page images sent to VLM parsing. Higher values reduce compression artifacts at larger payload sizes.\",\n \"default\": 85,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 100\n },\n \"prompt\": {\n \"description\": \"Custom extraction prompt\",\n \"type\": \"string\"\n },\n \"languages\": {\n \"description\": \"OCR language hints\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"outputFormat\": {\n \"description\": \"Format for extracted text. `markdown` (default) keeps structure; `plain` is unstyled text; `djot`/`html` preserve more markup. `layout` is native-PDF spatial text with column gaps preserved as spaces. It requires `parseMode: native` or `parseMode: native-or-ocr` and a PDF — omitted parseMode and `nativeText: true` are not enough. Native mode is layout-only and fail-closed. `native-or-ocr` keeps layout text on accepted native pages and uses OCR markdown on suspect or empty pages, so the document can mix fixed-width layout and markdown. Extractor failure never falls back to another parser. Office, plaintext, and images fail. OCR/vision cannot request `layout`.\",\n \"default\": \"markdown\",\n \"type\": \"string\",\n \"enum\": [\n \"plain\",\n \"markdown\",\n \"djot\",\n \"html\",\n \"layout\"\n ]\n },\n \"nativeText\": {\n \"description\": \"Extract native/embedded text from PDFs without OCR/VLM. Faster and uses no credits. Falls back to OCR/VLM if the PDF has no embedded text.\",\n \"default\": false,\n \"type\": \"boolean\"\n },\n \"describeFigures\": {\n \"description\": \"Opt-in (default off). After text extraction, detect which pages contain figures with an in-worker layout model, then caption those pages with a vision model and append `<figure>description</figure>` to their text — so image-only pages (property photos, signatures, charts) become findable by text-based steps like ai.split. Note: the layout scan runs over all pages, and the caption step and its vision calls are billed. Skipped for plaintext.\",\n \"type\": \"boolean\"\n },\n \"figureInstructions\": {\n \"description\": \"Custom instruction for the figure-description pass, e.g. \\\"Describe each figure; label a handwritten signature as `<figure>signature</figure>` and a stamp as `<figure>stamp</figure>`; for property photos note the room or exterior shown.\\\" Applied only when describeFigures runs.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"input\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"document\": {\n \"type\": \"object\",\n \"properties\": {\n \"filename\": {\n \"type\": \"string\"\n },\n \"mimeType\": {\n \"type\": \"string\"\n },\n \"size\": {\n \"type\": \"number\",\n \"description\": \"File size in bytes\"\n },\n \"storageRef\": {\n \"description\": \"Storage reference for the original file\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"filename\",\n \"mimeType\",\n \"size\"\n ],\n \"additionalProperties\": false\n },\n \"usage\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageCount\": {\n \"type\": \"number\"\n },\n \"processingTimeMs\": {\n \"type\": \"number\"\n },\n \"ocrPagesProcessed\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"pageCount\"\n ],\n \"additionalProperties\": false\n },\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"number\",\n \"description\": \"0-based page index\"\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Extracted page text (markdown/HTML/plain, or spatial layout text)\"\n },\n \"pageName\": {\n \"description\": \"Page/sheet name (e.g., Excel sheet name)\",\n \"type\": \"string\"\n },\n \"layoutElements\": {\n \"description\": \"Semantic layout elements in reading order\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"paragraph\",\n \"title\",\n \"sectionHeading\",\n \"pageHeader\",\n \"pageFooter\",\n \"pageNumber\",\n \"footnote\",\n \"table\",\n \"figure\",\n \"formulaBlock\"\n ]\n },\n \"content\": {\n \"type\": \"string\"\n },\n \"table\": {\n \"type\": \"object\",\n \"properties\": {\n \"cells\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"content\": {\n \"type\": \"string\"\n },\n \"rowIndex\": {\n \"type\": \"number\"\n },\n \"columnIndex\": {\n \"type\": \"number\"\n },\n \"rowSpan\": {\n \"default\": 1,\n \"type\": \"number\"\n },\n \"columnSpan\": {\n \"default\": 1,\n \"type\": \"number\"\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"content\",\n \"rowIndex\",\n \"columnIndex\",\n \"rowSpan\",\n \"columnSpan\"\n ],\n \"additionalProperties\": false\n }\n },\n \"rowCount\": {\n \"type\": \"number\"\n },\n \"columnCount\": {\n \"type\": \"number\"\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"cells\",\n \"rowCount\",\n \"columnCount\"\n ],\n \"additionalProperties\": false\n },\n \"caption\": {\n \"type\": \"string\"\n },\n \"figureId\": {\n \"type\": \"string\"\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"type\",\n \"content\"\n ],\n \"additionalProperties\": false\n }\n },\n \"words\": {\n \"description\": \"Word-level positions\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"text\": {\n \"type\": \"string\"\n },\n \"confidence\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"text\"\n ],\n \"additionalProperties\": false\n }\n },\n \"lines\": {\n \"description\": \"Line-level positions\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"text\": {\n \"type\": \"string\"\n },\n \"confidence\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"text\"\n ],\n \"additionalProperties\": false\n }\n },\n \"tables\": {\n \"description\": \"Extracted tables\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"cells\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"content\": {\n \"type\": \"string\"\n },\n \"rowIndex\": {\n \"type\": \"number\"\n },\n \"columnIndex\": {\n \"type\": \"number\"\n },\n \"rowSpan\": {\n \"default\": 1,\n \"type\": \"number\"\n },\n \"columnSpan\": {\n \"default\": 1,\n \"type\": \"number\"\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"content\",\n \"rowIndex\",\n \"columnIndex\",\n \"rowSpan\",\n \"columnSpan\"\n ],\n \"additionalProperties\": false\n }\n },\n \"rowCount\": {\n \"type\": \"number\"\n },\n \"columnCount\": {\n \"type\": \"number\"\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"cells\",\n \"rowCount\",\n \"columnCount\"\n ],\n \"additionalProperties\": false\n }\n },\n \"width\": {\n \"description\": \"Page width\",\n \"type\": \"number\"\n },\n \"height\": {\n \"description\": \"Page height\",\n \"type\": \"number\"\n },\n \"unit\": {\n \"description\": \"Unit for width/height and bounding regions\",\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"confidence\": {\n \"description\": \"Overall page confidence\",\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"nativeTextQuality\": {\n \"description\": \"Per-page diagnosis of native-extracted text. Reports objectively detectable anomalies (empty, U+FFFD, forbidden controls, unassigned/noncharacter code points, heavy private-use). Does not certify semantic correctness — valid-looking wrong mappings and literal \\\"?\\\" cannot be distinguished from legitimate text.\",\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"enum\": [\n \"clean\",\n \"empty\",\n \"suspect\"\n ]\n },\n \"reasons\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"empty\",\n \"replacement-character\",\n \"lone-surrogate\",\n \"forbidden-control\",\n \"unassigned\",\n \"noncharacter\",\n \"private-use\"\n ]\n }\n },\n \"counts\": {\n \"type\": \"object\",\n \"properties\": {\n \"codePoints\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"nonWhitespace\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"replacement\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"loneSurrogate\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"forbiddenControl\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"unassigned\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"noncharacter\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"privateUse\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"codePoints\",\n \"nonWhitespace\",\n \"replacement\",\n \"loneSurrogate\",\n \"forbiddenControl\",\n \"unassigned\",\n \"noncharacter\",\n \"privateUse\"\n ],\n \"additionalProperties\": false\n },\n \"profile\": {\n \"type\": \"object\",\n \"properties\": {\n \"replacementRatio\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"loneSurrogateRatio\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"forbiddenControlRatio\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"unassignedRatio\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"noncharacterRatio\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"privateUseRatio\": {\n \"type\": \"number\",\n \"minimum\": 0\n }\n },\n \"required\": [\n \"replacementRatio\",\n \"loneSurrogateRatio\",\n \"forbiddenControlRatio\",\n \"unassignedRatio\",\n \"noncharacterRatio\",\n \"privateUseRatio\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"status\",\n \"reasons\",\n \"counts\",\n \"profile\"\n ],\n \"additionalProperties\": false\n },\n \"provenance\": {\n \"description\": \"Which extractor produced the final text/layout on this page\",\n \"type\": \"object\",\n \"properties\": {\n \"source\": {\n \"type\": \"string\",\n \"enum\": [\n \"native\",\n \"ocr\",\n \"vision\"\n ]\n }\n },\n \"required\": [\n \"source\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"pageIndex\",\n \"text\"\n ],\n \"additionalProperties\": false\n }\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Combined text from all pages\"\n },\n \"parserType\": {\n \"type\": \"string\",\n \"enum\": [\n \"plaintext\",\n \"office\",\n \"llm-vision\",\n \"ocr\"\n ]\n },\n \"parserVersion\": {\n \"type\": \"string\"\n },\n \"model\": {\n \"description\": \"Model used (for LLM/OCR parsers)\",\n \"type\": \"string\"\n },\n \"processingStrategy\": {\n \"description\": \"How this result was produced: `native` (local PDF text only), `ocr`, `vision`, or `hybrid` (native pages merged with OCR on fallback pages).\",\n \"type\": \"string\",\n \"enum\": [\n \"native\",\n \"ocr\",\n \"vision\",\n \"hybrid\"\n ]\n },\n \"structured\": {\n \"description\": \"Canonical structured document with ordered blocks, regions, bounding boxes, tables, figures, and chunks when supported by the parser\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"document\",\n \"usage\",\n \"pages\",\n \"text\",\n \"parserType\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `ai.extract` — Extract Data\n\nExtract structured data from text using AI with a JSON schema\n\n**Behavior and examples:** `eigenpal docs read steps/ai/extract`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | Text content or template expression |\n| `schema` | object | yes | | JSON Schema defining the structure to extract |\n| `prompt` | string | no | | Custom prompt template for extraction |\n| `provider` | string | no | | Provider ID (e.g., \"openai-gpt4o\") |\n| `model` | string | no | | Model override |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `maxInputTokens` | integer | no | | Max input tokens. Truncates input text and logs a warning when exceeded. Omit for no limit. |\n| `grounded` | boolean | no | | Grounding is ON by default: each schema field gets a source span + confidence (high=verbatim, medium=fuzzy, low=ungrounded) under a reserved `_grounding` output key, and fields whose value cannot be located in the source are flagged for human review. Values stay the reliable schema-typed ones. The pass runs through the workspace LLM (any provider) and chunks long documents automatically. Tri-state: unset (default) = on, degrading gracefully to deterministic text alignment (`_grounding._degraded: true`) if no grounding model is available; `true` = strict, the step fails when the grounding model cannot be resolved; `false` = off, no `_grounding` key at all. |\n| `groundingModel` | string | no | | Provider/model for the grounding pass. Defaults to the workspace default LLM. Any configured provider works; the pass only fails the step when `grounded: true` is set explicitly and no model resolves. |\n| `groundingReasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `groundingExamples` | array<object> | no | | Optional few-shot examples pinning grounding to verbatim source text per field. |\n| `reviewOn` | `\"medium_or_low\"` \\| `\"low_only\"` | no | | Which grounding confidences set needsReview on a field. Default: low_only (only fields whose value could not be located in the source). Use medium_or_low to also flag approximate and derived matches. |\n\n**Output:** `record<string, unknown>`\n\n> Extracted structured data matching the provided schema. Unless grounding is disabled (grounded: false), the output also carries a reserved `_grounding` map keyed by dotted field path: `_grounding[\"line_items.0.amount\"] = { confidence: high|medium|low, score?, needsReview, reason?, quote?, citations?, source_span }`, plus reserved `_degraded: true` / `_reason` markers when grounding could not run the shared algorithm or the Eigenpal text pass. Lineage is never included in this JSON.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"Text content or template expression\"\n },\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"object\"\n },\n \"properties\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"required\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"properties\"\n ],\n \"additionalProperties\": false,\n \"description\": \"JSON Schema defining the structure to extract\"\n },\n \"prompt\": {\n \"description\": \"Custom prompt template for extraction\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID (e.g., \\\"openai-gpt4o\\\")\",\n \"type\": \"string\"\n },\n \"model\": {\n \"description\": \"Model override\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"maxInputTokens\": {\n \"description\": \"Max input tokens. Truncates input text and logs a warning when exceeded. Omit for no limit.\",\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"grounded\": {\n \"description\": \"Grounding is ON by default: each schema field gets a source span + confidence (high=verbatim, medium=fuzzy, low=ungrounded) under a reserved `_grounding` output key, and fields whose value cannot be located in the source are flagged for human review. Values stay the reliable schema-typed ones. The pass runs through the workspace LLM (any provider) and chunks long documents automatically. Tri-state: unset (default) = on, degrading gracefully to deterministic text alignment (`_grounding._degraded: true`) if no grounding model is available; `true` = strict, the step fails when the grounding model cannot be resolved; `false` = off, no `_grounding` key at all.\",\n \"type\": \"boolean\"\n },\n \"groundingModel\": {\n \"description\": \"Provider/model for the grounding pass. Defaults to the workspace default LLM. Any configured provider works; the pass only fails the step when `grounded: true` is set explicitly and no model resolves.\",\n \"type\": \"string\"\n },\n \"groundingReasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"groundingExamples\": {\n \"description\": \"Optional few-shot examples pinning grounding to verbatim source text per field.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"text\": {\n \"type\": \"string\"\n },\n \"extractions\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"field\": {\n \"type\": \"string\"\n },\n \"text\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"field\",\n \"text\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"text\",\n \"extractions\"\n ],\n \"additionalProperties\": false\n }\n },\n \"reviewOn\": {\n \"description\": \"Which grounding confidences set needsReview on a field. Default: low_only (only fields whose value could not be located in the source). Use medium_or_low to also flag approximate and derived matches.\",\n \"type\": \"string\",\n \"enum\": [\n \"medium_or_low\",\n \"low_only\"\n ]\n }\n },\n \"required\": [\n \"input\",\n \"schema\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Extracted structured data matching the provided schema. Unless grounding is disabled (grounded: false), the output also carries a reserved `_grounding` map keyed by dotted field path: `_grounding[\\\"line_items.0.amount\\\"] = { confidence: high|medium|low, score?, needsReview, reason?, quote?, citations?, source_span }`, plus reserved `_degraded: true` / `_reason` markers when grounding could not run the shared algorithm or the Eigenpal text pass. Lineage is never included in this JSON.\"\n}\n```\n\n\n#### `ai.split` — Split Document\n\nSplit a parsed document into named sections using an LLM. Consumes ai.parse output; emits per-section page ranges and text ready for downstream ai.extract via control.parallel_map.\n\n**Behavior and examples:** `eigenpal docs read steps/ai/split`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | Template expression resolving to ai.parse output, e.g. \"{{ steps.parse.output }}\" |\n| `sections` | array<object> | yes | | Named sections to find in the document |\n| `rules` | string | no | | Optional natural-language rules appended to the system prompt. E.g. \"End-of-section markers like *Koniec prílohy 2* close the current section.\" |\n| `provider` | string | no | | Provider ID from eigenpal.config.yaml (e.g. \"openai-gpt5.4-mini\"). Falls back to the tenant default LLM provider when omitted. |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `windowTokenBudget` | integer | no | | Override the estimated per-window token ceiling. Oversized pages are subdivided with overlap while retaining their original page index. Defaults to env SPLIT_WINDOW_TOKEN_BUDGET or 20000. Inputs requiring more than 64 LLM windows are rejected before calls begin. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `splits` | array<object> | yes | | Sections found in the document, in page order. Absent sections are omitted. |\n| `sections` | record<string, object> | yes | | The same sections keyed by config name, so a downstream step can reference one directly: `{{ steps.<split>.output.sections.<name>.page_range }}`. Prefer this over filtering `splits`. On a duplicate name the last wins. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"Template expression resolving to ai.parse output, e.g. \\\"{{ steps.parse.output }}\\\"\"\n },\n \"sections\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Stable id, used as the key in output and template references\"\n },\n \"description\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"What this section looks like in the document. Use the document's own terminology (e.g. \\\"Príloha 2 / Anlage 2\\\") and visual cues. Multilingual variants belong here.\"\n },\n \"required\": {\n \"description\": \"If true, log a warn when this section is not found, and lean the LLM toward expecting it. Default: false (sections may be absent).\",\n \"type\": \"boolean\"\n },\n \"endHints\": {\n \"description\": \"Natural-language hints fed to the LLM about what marks the END of this section (e.g. [\\\"PRÍPAD PORUŠENIA ZMLUVY\\\", \\\"*Koniec prílohy*\\\"]). The LLM uses these as guidance — not a regex match — so casing variants, missing diacritics, and multilingual phrasings all close the section correctly.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"name\",\n \"description\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Named sections to find in the document\"\n },\n \"rules\": {\n \"description\": \"Optional natural-language rules appended to the system prompt. E.g. \\\"End-of-section markers like *Koniec prílohy 2* close the current section.\\\"\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID from eigenpal.config.yaml (e.g. \\\"openai-gpt5.4-mini\\\"). Falls back to the tenant default LLM provider when omitted.\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"windowTokenBudget\": {\n \"description\": \"Override the estimated per-window token ceiling. Oversized pages are subdivided with overlap while retaining their original page index. Defaults to env SPLIT_WINDOW_TOKEN_BUDGET or 20000. Inputs requiring more than 64 LLM windows are rejected before calls begin.\",\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"input\",\n \"sections\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"splits\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Section name from the config\"\n },\n \"page_range\": {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n ],\n \"description\": \"[startIndex, endIndex] inclusive, 0-based page indices\"\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ],\n \"description\": \"LLM confidence at the anchor page. Coarse enum (low | medium | high) — numeric scores cluster meaninglessly at 0.85-0.95.\"\n },\n \"notes\": {\n \"type\": \"string\",\n \"description\": \"LLM's justification for the anchor — useful for debugging\"\n },\n \"evidence\": {\n \"description\": \"Structured start-anchor evidence — parse this instead of regexing over `notes`.\",\n \"type\": \"object\",\n \"properties\": {\n \"start_heading_text\": {\n \"description\": \"Verbatim heading text the LLM cited as the section start\",\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"start_page\": {\n \"description\": \"Page where start_heading_text appears\",\n \"anyOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"end_evidence\": {\n \"description\": \"Set when the LLM detected an end-of-section cue (matched against the section's `endHints` or an explicit closing marker). Absent when the section was closed by continuity-fill alone.\",\n \"type\": \"object\",\n \"properties\": {\n \"end_page\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"LAST page of the section (inclusive)\"\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n \"notes\": {\n \"type\": \"string\",\n \"description\": \"LLM's justification for the end. Pair with start `notes` for reconciliation.\"\n }\n },\n \"required\": [\n \"end_page\",\n \"confidence\",\n \"notes\"\n ],\n \"additionalProperties\": false\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Joined per-page content for this section, ready for downstream ai.extract\"\n },\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"pageName\": {\n \"type\": \"string\"\n },\n \"source\": {\n \"type\": \"string\",\n \"enum\": [\n \"anchored\",\n \"inferred\"\n ],\n \"description\": \"Whether this page is direct LLM evidence (anchored) or continuity-filled (inferred)\"\n }\n },\n \"required\": [\n \"pageIndex\",\n \"text\",\n \"source\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Raw per-page records covered by this split (in page order). Iterate when downstream needs per-page context (e.g. control.parallel_map over section pages).\"\n },\n \"pages_anchored\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Pages with direct LLM evidence\"\n },\n \"pages_inferred\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Pages assigned by deterministic continuity fill\"\n }\n },\n \"required\": [\n \"name\",\n \"page_range\",\n \"confidence\",\n \"notes\",\n \"text\",\n \"pages\",\n \"pages_anchored\",\n \"pages_inferred\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Sections found in the document, in page order. Absent sections are omitted.\"\n },\n \"sections\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Section name from the config\"\n },\n \"page_range\": {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n ],\n \"description\": \"[startIndex, endIndex] inclusive, 0-based page indices\"\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ],\n \"description\": \"LLM confidence at the anchor page. Coarse enum (low | medium | high) — numeric scores cluster meaninglessly at 0.85-0.95.\"\n },\n \"notes\": {\n \"type\": \"string\",\n \"description\": \"LLM's justification for the anchor — useful for debugging\"\n },\n \"evidence\": {\n \"description\": \"Structured start-anchor evidence — parse this instead of regexing over `notes`.\",\n \"type\": \"object\",\n \"properties\": {\n \"start_heading_text\": {\n \"description\": \"Verbatim heading text the LLM cited as the section start\",\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"start_page\": {\n \"description\": \"Page where start_heading_text appears\",\n \"anyOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"end_evidence\": {\n \"description\": \"Set when the LLM detected an end-of-section cue (matched against the section's `endHints` or an explicit closing marker). Absent when the section was closed by continuity-fill alone.\",\n \"type\": \"object\",\n \"properties\": {\n \"end_page\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"LAST page of the section (inclusive)\"\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n \"notes\": {\n \"type\": \"string\",\n \"description\": \"LLM's justification for the end. Pair with start `notes` for reconciliation.\"\n }\n },\n \"required\": [\n \"end_page\",\n \"confidence\",\n \"notes\"\n ],\n \"additionalProperties\": false\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Joined per-page content for this section, ready for downstream ai.extract\"\n },\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"pageName\": {\n \"type\": \"string\"\n },\n \"source\": {\n \"type\": \"string\",\n \"enum\": [\n \"anchored\",\n \"inferred\"\n ],\n \"description\": \"Whether this page is direct LLM evidence (anchored) or continuity-filled (inferred)\"\n }\n },\n \"required\": [\n \"pageIndex\",\n \"text\",\n \"source\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Raw per-page records covered by this split (in page order). Iterate when downstream needs per-page context (e.g. control.parallel_map over section pages).\"\n },\n \"pages_anchored\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Pages with direct LLM evidence\"\n },\n \"pages_inferred\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Pages assigned by deterministic continuity fill\"\n }\n },\n \"required\": [\n \"name\",\n \"page_range\",\n \"confidence\",\n \"notes\",\n \"text\",\n \"pages\",\n \"pages_anchored\",\n \"pages_inferred\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"The same sections keyed by config name, so a downstream step can reference one directly: `{{ steps.<split>.output.sections.<name>.page_range }}`. Prefer this over filtering `splits`. On a duplicate name the last wins.\"\n }\n },\n \"required\": [\n \"splits\",\n \"sections\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `ai.segment` — Separate Documents\n\nSeparate a concatenated batch (one big scan) into typed document instances using an LLM. Consumes ai.parse output and a type taxonomy; discovers an unknown number of documents in any order and emits per-document page ranges + text + type, ready for type-specific ai.extract via control.parallel_map. The inverse of ai.split.\n\n**Behavior and examples:** `eigenpal docs read steps/ai/segment`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | Template expression resolving to ai.parse output, e.g. \"{{ steps.parse.output }}\" |\n| `documentTypes` | array<object> | yes | | The document-type taxonomy. The LLM tags each detected document with one of these names, or the reserved \"unknown\" type when none fit. |\n| `rules` | string | no | | Optional natural-language rules appended to the system prompt. |\n| `provider` | string | no | | Provider ID from eigenpal.config.yaml. Falls back to the tenant default LLM provider when omitted. |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `windowTokenBudget` | integer | no | | Override the per-window token ceiling. Defaults to env SPLIT_WINDOW_TOKEN_BUDGET or 20000. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `summary` | object | yes | | Batch-level escalation summary: labelled vs unlabelled counts + docs needing review. |\n| `documents` | array<object> | yes | | Documents discovered in the batch, in page order. The full batch is covered. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"Template expression resolving to ai.parse output, e.g. \\\"{{ steps.parse.output }}\\\"\"\n },\n \"documentTypes\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Type label returned in documents[].type (e.g. \\\"timesheet\\\", \\\"deal-memo\\\")\"\n },\n \"description\": {\n \"description\": \"What this document type looks like — headings, layout, vocabulary, and the cues that mark its FIRST page. Fed to the LLM for both boundary detection and typing.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"The document-type taxonomy. The LLM tags each detected document with one of these names, or the reserved \\\"unknown\\\" type when none fit.\"\n },\n \"rules\": {\n \"description\": \"Optional natural-language rules appended to the system prompt.\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID from eigenpal.config.yaml. Falls back to the tenant default LLM provider when omitted.\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"windowTokenBudget\": {\n \"description\": \"Override the per-window token ceiling. Defaults to env SPLIT_WINDOW_TOKEN_BUDGET or 20000.\",\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"input\",\n \"documentTypes\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"summary\": {\n \"type\": \"object\",\n \"properties\": {\n \"total\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"Total documents discovered.\"\n },\n \"labelled\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"Documents assigned a known taxonomy type.\"\n },\n \"unlabelled\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"Documents the segmenter could not label (type \\\"unknown\\\") — escalate to a human.\"\n },\n \"lowConfidence\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"Documents with low boundary/type confidence.\"\n },\n \"byType\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Per-type document counts.\"\n },\n \"needsReview\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"index\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"type\": {\n \"type\": \"string\"\n },\n \"page_range\": {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n ]\n },\n \"reason\": {\n \"type\": \"string\",\n \"enum\": [\n \"unlabelled\",\n \"low_confidence\"\n ]\n },\n \"notes\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"index\",\n \"type\",\n \"page_range\",\n \"reason\",\n \"notes\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Documents to escalate to a human operator, with the reason for each.\"\n }\n },\n \"required\": [\n \"total\",\n \"labelled\",\n \"unlabelled\",\n \"lowConfidence\",\n \"byType\",\n \"needsReview\"\n ],\n \"additionalProperties\": false,\n \"description\": \"Batch-level escalation summary: labelled vs unlabelled counts + docs needing review.\"\n },\n \"documents\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"index\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"0-based position of this document in the batch\"\n },\n \"type\": {\n \"type\": \"string\",\n \"description\": \"Chosen type name from the taxonomy, or \\\"unknown\\\"\"\n },\n \"page_range\": {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n ],\n \"description\": \"[startIndex, endIndex] inclusive, 0-based page indices\"\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ],\n \"description\": \"LLM confidence at the boundary + type. Coarse enum (low | medium | high).\"\n },\n \"notes\": {\n \"type\": \"string\",\n \"description\": \"LLM justification for the boundary — useful for debugging\"\n },\n \"evidence\": {\n \"description\": \"Structured start evidence — parse this instead of regexing over `notes`.\",\n \"type\": \"object\",\n \"properties\": {\n \"start_heading_text\": {\n \"description\": \"Verbatim heading/title text the LLM cited as the document start\",\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"start_page\": {\n \"description\": \"Page where the start appears\",\n \"anyOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Joined per-page content for this document, ready for downstream ai.extract\"\n },\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"pageName\": {\n \"type\": \"string\"\n },\n \"source\": {\n \"type\": \"string\",\n \"enum\": [\n \"anchored\",\n \"inferred\"\n ]\n }\n },\n \"required\": [\n \"pageIndex\",\n \"text\",\n \"source\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Raw per-page records covered by this document, in page order.\"\n },\n \"pages_anchored\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Pages with direct LLM boundary evidence\"\n },\n \"pages_inferred\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Pages assigned by deterministic continuity fill\"\n }\n },\n \"required\": [\n \"index\",\n \"type\",\n \"page_range\",\n \"confidence\",\n \"notes\",\n \"text\",\n \"pages\",\n \"pages_anchored\",\n \"pages_inferred\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Documents discovered in the batch, in page order. The full batch is covered.\"\n }\n },\n \"required\": [\n \"summary\",\n \"documents\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `ai.classify` — Classify\n\nClassify a document or text into one of a fixed label set using an LLM. Output exposes the picked label (constrained to the configured names), a coarse confidence, and a short justification. Pair with control.fail to reject documents that match an undesired label.\n\n**Behavior and examples:** `eigenpal docs read steps/ai/classify`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | Template expression for the text to classify. Typically the output of ai.parse, e.g. \"{{ steps.parse.output.text }}\". |\n| `labels` | array<object> | yes | | Allowed labels. The LLM is constrained to pick exactly one of these names. |\n| `prompt` | string | no | | Custom classification instructions appended to the system prompt. Use to clarify edge cases or emphasize evidence the model should weigh. |\n| `provider` | string | no | | Provider ID from eigenpal.config.yaml (e.g. \"openai-gpt4o-mini\"). Falls back to the tenant default LLM provider when omitted. |\n| `model` | string | no | | Model override (advanced) |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `maxInputTokens` | integer | no | | Max input tokens. Truncates input text when exceeded. Omit for no limit. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `label` | string | yes | | The selected label name (one of the configured labels). Compare against literal strings to gate downstream steps. |\n| `confidence` | `\"low\"` \\| `\"medium\"` \\| `\"high\"` | yes | | LLM confidence in the classification. Coarse enum — numeric scores cluster meaninglessly at 0.85-0.95. |\n| `reason` | string | yes | | Short justification for the chosen label — useful for debugging. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"Template expression for the text to classify. Typically the output of ai.parse, e.g. \\\"{{ steps.parse.output.text }}\\\".\"\n },\n \"labels\": {\n \"minItems\": 2,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"The label value returned in output.label. Keep it short and stable (used in downstream conditions).\"\n },\n \"description\": {\n \"description\": \"What documents fall into this label. Fed to the LLM as guidance.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Allowed labels. The LLM is constrained to pick exactly one of these names.\"\n },\n \"prompt\": {\n \"description\": \"Custom classification instructions appended to the system prompt. Use to clarify edge cases or emphasize evidence the model should weigh.\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID from eigenpal.config.yaml (e.g. \\\"openai-gpt4o-mini\\\"). Falls back to the tenant default LLM provider when omitted.\",\n \"type\": \"string\"\n },\n \"model\": {\n \"description\": \"Model override (advanced)\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"maxInputTokens\": {\n \"description\": \"Max input tokens. Truncates input text when exceeded. Omit for no limit.\",\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"input\",\n \"labels\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"label\": {\n \"type\": \"string\",\n \"description\": \"The selected label name (one of the configured labels). Compare against literal strings to gate downstream steps.\"\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ],\n \"description\": \"LLM confidence in the classification. Coarse enum — numeric scores cluster meaninglessly at 0.85-0.95.\"\n },\n \"reason\": {\n \"type\": \"string\",\n \"description\": \"Short justification for the chosen label — useful for debugging.\"\n }\n },\n \"required\": [\n \"label\",\n \"confidence\",\n \"reason\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `ai.classify-pages` — Label Pages\n\nAssign zero or more labels to each page independently (multi-label) using an LLM. Consumes ai.parse output; emits per-page labels and a byLabel map (label -> page indices) that supports NON-contiguous selections. Feed byLabel.<label> straight into ai.vision `pageIndices` to inspect scattered pages of a type (e.g. every signature or property-photo page).\n\n**Behavior and examples:** `eigenpal docs read steps/ai/classify-pages`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | The ai.parse output to label, e.g. \"{{ steps.parse.output }}\". Each page is labelled independently. |\n| `labels` | array<string \\| object> | yes | | Labels a page can carry. A page may match ZERO, ONE, or MANY of them. Each entry is a bare name (e.g. \"photo\") or { name, description }. Names must be unique. |\n| `prompt` | string | no | | Extra classification guidance appended to the system prompt. |\n| `provider` | string | no | | Provider ID from eigenpal.config.yaml. Falls back to the tenant default. |\n| `model` | string | no | | Model override (advanced). |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `windowTokenBudget` | integer | no | | Per-window token ceiling. Pages are packed into windows under this budget; one LLM call per window. Defaults to env SPLIT_WINDOW_TOKEN_BUDGET (20000). |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `pages` | array<object> | yes | | Every input page, in order, with the labels the model assigned to it. |\n| `byLabel` | record<string, array<integer>> | yes | | Label name -> ascending page indices carrying it. Every configured label is present (empty array when no page matched), so dot access is always safe: `{{ steps.<step>.output.byLabel.<label> }}` resolves to a number[] ready for ai.vision `pageIndices`. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"The ai.parse output to label, e.g. \\\"{{ steps.parse.output }}\\\". Each page is labelled independently.\"\n },\n \"labels\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Label value — the key in output.byLabel and an entry in pages[].labels.\"\n },\n \"description\": {\n \"description\": \"What a page must show to earn this label. Fed to the LLM as guidance.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\"\n ],\n \"additionalProperties\": false\n }\n ]\n },\n \"description\": \"Labels a page can carry. A page may match ZERO, ONE, or MANY of them. Each entry is a bare name (e.g. \\\"photo\\\") or { name, description }. Names must be unique.\"\n },\n \"prompt\": {\n \"description\": \"Extra classification guidance appended to the system prompt.\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID from eigenpal.config.yaml. Falls back to the tenant default.\",\n \"type\": \"string\"\n },\n \"model\": {\n \"description\": \"Model override (advanced).\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"windowTokenBudget\": {\n \"description\": \"Per-window token ceiling. Pages are packed into windows under this budget; one LLM call per window. Defaults to env SPLIT_WINDOW_TOKEN_BUDGET (20000).\",\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"input\",\n \"labels\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"labels\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Labels assigned to this page (possibly empty).\"\n }\n },\n \"required\": [\n \"pageIndex\",\n \"labels\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Every input page, in order, with the labels the model assigned to it.\"\n },\n \"byLabel\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n },\n \"description\": \"Label name -> ascending page indices carrying it. Every configured label is present (empty array when no page matched), so dot access is always safe: `{{ steps.<step>.output.byLabel.<label> }}` resolves to a number[] ready for ai.vision `pageIndices`.\"\n }\n },\n \"required\": [\n \"pages\",\n \"byLabel\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `ai.vision` — Inspect Pages (Vision)\n\nInspect rendered page images with a vision model and return structured JSON matching a schema. The visual counterpart to Extract Data: use it for conclusions that live in the pixels rather than the text (is the document signed? are the photos usable?). Renders PDF, image, or Office/Word inputs; route to specific pages with an ai.split page range to keep it cheap.\n\n**Behavior and examples:** `eigenpal docs read steps/ai/vision`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `document` | string | yes | | Template expression resolving to the input file (PDF, image, or Office document). |\n| `pageFrom` | integer \\| string | no | | First page to inspect (0-based, inclusive). Optional. Accepts a template expression. When omitted, starts from the first page. |\n| `pageTo` | integer \\| string | no | | Last page to inspect (0-based, inclusive). Optional. Accepts a template expression. When omitted, runs to the last page. When BOTH pageFrom and pageTo are omitted, the whole document is inspected in chunks of maxPages (divide-and-conquer, results merged). |\n| `pageIndices` | array<integer> \\| string | no | | Explicit list of 0-based page indices to inspect — supports NON-contiguous selections, e.g. [4, 11, 19], or a template resolving to a number[] such as \"{{ steps.label_pages.output.byLabel.photo }}\". Highest priority: when set, pageFrom/pageTo and pages are ignored. |\n| `schema` | object | yes | | JSON Schema defining the structure the vision model should return. |\n| `prompt` | string | no | | Optional instruction refining the extraction. The schema drives it when omitted. |\n| `provider` | string | no | | Provider ID (must support vision). |\n| `model` | string | no | | Model override (must support vision). |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `renderScale` | number | no | | PDF render scale (1.0 = 72 DPI). Raise for small text or weak VLM OCR. Capped at 6 to avoid oversized page rasters. |\n| `imageQuality` | integer | no | | JPEG quality for rendered pages (default 85). |\n| `maxPages` | integer | no | | Chunk size: the maximum pages sent to the vision model in a single call. When the inspected range (or the whole document) is larger, it is split into chunks of this size and the per-chunk results are merged by a reduce pass. The reduce follows each field's DESCRIPTION: a boolean described as \"present on any page\" is OR-ed, one described as \"true for every page\" is AND-ed, and list fields are concatenated. The merge is most reliable for boolean/scalar claims; for schemas that AGGREGATE long lists across chunks it can still drop or reorder items, so prefer a bounded page range for large list extraction. Default 20, capped at 100. |\n\n**Output:** `record<string, unknown>`\n\n> Structured data matching the provided schema. The output also carries a reserved `_vision` key recording the inspected source document ref and the page indices that were rendered, so the UI can re-rasterize exactly those pages client-side. `_vision` is excluded from user-schema validation.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"document\": {\n \"type\": \"string\",\n \"description\": \"Template expression resolving to the input file (PDF, image, or Office document).\"\n },\n \"pageFrom\": {\n \"description\": \"First page to inspect (0-based, inclusive). Optional. Accepts a template expression. When omitted, starts from the first page.\",\n \"anyOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"pageTo\": {\n \"description\": \"Last page to inspect (0-based, inclusive). Optional. Accepts a template expression. When omitted, runs to the last page. When BOTH pageFrom and pageTo are omitted, the whole document is inspected in chunks of maxPages (divide-and-conquer, results merged).\",\n \"anyOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"pageIndices\": {\n \"description\": \"Explicit list of 0-based page indices to inspect — supports NON-contiguous selections, e.g. [4, 11, 19], or a template resolving to a number[] such as \\\"{{ steps.label_pages.output.byLabel.photo }}\\\". Highest priority: when set, pageFrom/pageTo and pages are ignored.\",\n \"anyOf\": [\n {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"pages\": {\n \"description\": \"DEPRECATED — prefer pageIndices (scattered) or pageFrom/pageTo (contiguous). Which pages to inspect: \\\"all\\\", a single [start,end] range, a list of ranges, or a template expression resolving to one of those.\",\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"all\"\n },\n {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n }\n ],\n \"description\": \"Inclusive 0-based [start, end] page range, matching ai.split page_range.\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n }\n ],\n \"description\": \"Inclusive 0-based [start, end] page range, matching ai.split page_range.\"\n }\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"object\"\n },\n \"properties\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"required\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"properties\"\n ],\n \"additionalProperties\": false,\n \"description\": \"JSON Schema defining the structure the vision model should return.\"\n },\n \"prompt\": {\n \"description\": \"Optional instruction refining the extraction. The schema drives it when omitted.\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID (must support vision).\",\n \"type\": \"string\"\n },\n \"model\": {\n \"description\": \"Model override (must support vision).\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"renderScale\": {\n \"description\": \"PDF render scale (1.0 = 72 DPI). Raise for small text or weak VLM OCR. Capped at 6 to avoid oversized page rasters.\",\n \"type\": \"number\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 6\n },\n \"imageQuality\": {\n \"description\": \"JPEG quality for rendered pages (default 85).\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 100\n },\n \"maxPages\": {\n \"description\": \"Chunk size: the maximum pages sent to the vision model in a single call. When the inspected range (or the whole document) is larger, it is split into chunks of this size and the per-chunk results are merged by a reduce pass. The reduce follows each field's DESCRIPTION: a boolean described as \\\"present on any page\\\" is OR-ed, one described as \\\"true for every page\\\" is AND-ed, and list fields are concatenated. The merge is most reliable for boolean/scalar claims; for schemas that AGGREGATE long lists across chunks it can still drop or reorder items, so prefer a bounded page range for large list extraction. Default 20, capped at 100.\",\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 100\n }\n },\n \"required\": [\n \"document\",\n \"schema\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Structured data matching the provided schema. The output also carries a reserved `_vision` key recording the inspected source document ref and the page indices that were rendered, so the UI can re-rasterize exactly those pages client-side. `_vision` is excluded from user-schema validation.\"\n}\n```\n\n\n#### `ai.search-files` — Search Files\n\nInvestigate a ZIP archive with a question and return an answer plus file citations. Read-only: the step opens files in the archive and never writes back. ZIP is the only accepted format in this version. Optionally scope the search to a folder prefix and cap how many files and investigation turns may run.\n\n**Behavior and examples:** `eigenpal docs read steps/ai/search-files`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `archive` | string | yes | | Template expression or file reference for the ZIP archive to inspect. ZIP is the only accepted format in this version. |\n| `query` | string | yes | | Natural-language question to answer from files inside the archive. |\n| `root` | string | no | | Optional folder prefix inside the archive. When set, only files under this path are in scope. |\n| `provider` | string | no | | Provider ID from eigenpal.config.yaml. Falls back to the tenant default LLM provider when omitted. |\n| `model` | string | no | | Model override (advanced). |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `maxIterations` | integer | no | `12` | Maximum investigation turns. Default 12, capped at 50. |\n| `maxFiles` | integer | no | `50` | Maximum files the step may open. Default 50, capped at 200. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `answer` | string | yes | | The investigation answer. Citations point at inspected files; the answer text itself is model-generated. |\n| `citations` | array<object> | yes | | Citations to files the step inspected. Excerpts, when present, are copied from inspected text. |\n| `filesInspected` | array<string> | yes | | Archive-relative paths of files the step opened during investigation. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"archive\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Template expression or file reference for the ZIP archive to inspect. ZIP is the only accepted format in this version.\"\n },\n \"query\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Natural-language question to answer from files inside the archive.\"\n },\n \"root\": {\n \"description\": \"Optional folder prefix inside the archive. When set, only files under this path are in scope.\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID from eigenpal.config.yaml. Falls back to the tenant default LLM provider when omitted.\",\n \"type\": \"string\"\n },\n \"model\": {\n \"description\": \"Model override (advanced).\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"maxIterations\": {\n \"description\": \"Maximum investigation turns. Default 12, capped at 50.\",\n \"default\": 12,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 50\n },\n \"maxFiles\": {\n \"description\": \"Maximum files the step may open. Default 50, capped at 200.\",\n \"default\": 50,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 200\n }\n },\n \"required\": [\n \"archive\",\n \"query\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"answer\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"The investigation answer. Citations point at inspected files; the answer text itself is model-generated.\"\n },\n \"citations\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"path\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Archive-relative path of a file the step inspected and cited.\"\n },\n \"excerpt\": {\n \"description\": \"Optional excerpt copied from the inspected file text. Omitted when no verbatim excerpt was supplied.\",\n \"type\": \"string\",\n \"maxLength\": 2000\n }\n },\n \"required\": [\n \"path\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Citations to files the step inspected. Excerpts, when present, are copied from inspected text.\"\n },\n \"filesInspected\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Archive-relative paths of files the step opened during investigation.\"\n }\n },\n \"required\": [\n \"answer\",\n \"citations\",\n \"filesInspected\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `ai.figure-crops` — Figure Crops\n\nCrop and caption figure elements from a parsed PDF. Renders the source document, crops each kind=figure bbox from ai.parse output, and describes the crop with a vision model. Output is captions plus normalized bboxes for evidence viewers.\n\n**Behavior and examples:** `eigenpal docs read steps/ai/figure-crops`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | Storage reference or template expression for the source PDF |\n| `parseOutput` | unknown | yes | | ai.parse output object (template {{steps.<parse>.output}}); figure elements are read from its `elements` array |\n| `model` | string | no | | Vision model for captions (defaults to workspace model) |\n| `figureInstructions` | string | no | | Custom caption instruction; applied per figure crop |\n| `maxFigures` | integer | no | `40` | Cap on figures processed per document |\n| `minAreaFrac` | number | no | `0` | Skip figures below this page-area fraction (logos/decorations) |\n| `includeCrops` | boolean | no | `false` | Include base64 JPEG crops in output (heavy; default off — captions + bboxes only) |\n| `renderScale` | number | no | `1` | Scale factor for rendering PDF pages before cropping |\n| `imageQuality` | integer | no | `85` | JPEG quality for crops |\n| `llmReasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `figures` | array<object> | yes | | |\n| `_figureCrops` | object | yes | | |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"Storage reference or template expression for the source PDF\"\n },\n \"parseOutput\": {\n \"description\": \"ai.parse output object (template {{steps.<parse>.output}}); figure elements are read from its `elements` array\"\n },\n \"model\": {\n \"description\": \"Vision model for captions (defaults to workspace model)\",\n \"type\": \"string\"\n },\n \"figureInstructions\": {\n \"description\": \"Custom caption instruction; applied per figure crop\",\n \"type\": \"string\"\n },\n \"maxFigures\": {\n \"description\": \"Cap on figures processed per document\",\n \"default\": 40,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 100\n },\n \"minAreaFrac\": {\n \"description\": \"Skip figures below this page-area fraction (logos/decorations)\",\n \"default\": 0,\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"includeCrops\": {\n \"description\": \"Include base64 JPEG crops in output (heavy; default off — captions + bboxes only)\",\n \"default\": false,\n \"type\": \"boolean\"\n },\n \"renderScale\": {\n \"description\": \"Scale factor for rendering PDF pages before cropping\",\n \"default\": 1,\n \"type\": \"number\",\n \"minimum\": 1,\n \"maximum\": 4\n },\n \"imageQuality\": {\n \"description\": \"JPEG quality for crops\",\n \"default\": 85,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 100\n },\n \"llmReasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n }\n },\n \"required\": [\n \"input\",\n \"parseOutput\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"figures\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"page\": {\n \"type\": \"number\"\n },\n \"bboxNorm\": {\n \"anyOf\": [\n {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"number\"\n }\n ]\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"areaFrac\": {\n \"type\": \"number\"\n },\n \"caption\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"cropJpegBase64\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"id\",\n \"page\",\n \"bboxNorm\",\n \"areaFrac\",\n \"caption\"\n ],\n \"additionalProperties\": false\n }\n },\n \"_figureCrops\": {\n \"type\": \"object\",\n \"properties\": {\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n }\n },\n \"model\": {\n \"type\": \"string\"\n },\n \"skippedSmall\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"pages\",\n \"skippedSmall\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"figures\",\n \"_figureCrops\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n### Transform steps — deterministic data transforms\n\n#### `transform.set` — Set Value\n\nSet key-value pairs in the output object\n\n**Behavior and examples:** `eigenpal docs read steps/transform/set`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `fields` | record<string, unknown> | yes | | Key-value pairs to set in output |\n| `input` | record<string, unknown> | no | | Base object to extend |\n\n**Output:** `record<string, unknown>`\n\n> Object with all fields set\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"fields\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Key-value pairs to set in output\"\n },\n \"input\": {\n \"description\": \"Base object to extend\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"fields\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Object with all fields set\"\n}\n```\n\n\n#### `transform.remove` — Remove Fields\n\nRemove specified fields from an object\n\n**Behavior and examples:** `eigenpal docs read steps/transform/remove`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | record<string, unknown> | no | | Object to remove fields from |\n| `fields` | array<string> | yes | | Field names to remove |\n\n**Output:** `record<string, unknown>`\n\n> Object with fields removed\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"description\": \"Object to remove fields from\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"fields\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Field names to remove\"\n }\n },\n \"required\": [\n \"fields\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Object with fields removed\"\n}\n```\n\n\n#### `transform.combine` — Combine Data\n\nMerge multiple objects or concatenate arrays\n\n**Behavior and examples:** `eigenpal docs read steps/transform/combine`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `sources` | array<string> | yes | | Template expressions for sources to combine |\n| `target` | string | yes | | Target path in output |\n| `mode` | `\"merge\"` \\| `\"concat\"` \\| `\"deep\"` | no | `\"merge\"` | |\n\n**Output:** `record<string, unknown> \\| array<unknown>`\n\n> Combined result\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"sources\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Template expressions for sources to combine\"\n },\n \"target\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Target path in output\"\n },\n \"mode\": {\n \"default\": \"merge\",\n \"type\": \"string\",\n \"enum\": [\n \"merge\",\n \"concat\",\n \"deep\"\n ]\n }\n },\n \"required\": [\n \"sources\",\n \"target\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n {\n \"type\": \"array\",\n \"items\": {}\n }\n ],\n \"description\": \"Combined result\"\n}\n```\n\n\n#### `transform.split` — Split Data\n\nSplit a string by delimiter or extract keys from an object\n\n**Behavior and examples:** `eigenpal docs read steps/transform/split`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string \\| record<string, unknown> | no | | String or object to split |\n| `source` | string | no | | Template expression for source |\n| `by` | string | no | `\",\"` | Delimiter for string splitting |\n| `delimiter` | string | no | | Alias for by |\n| `limit` | number | no | | Max number of splits |\n| `keys` | array<string> | no | | Keys to extract from object |\n\n**Output:** `array<string> \\| object`\n\n> Split result - array or extracted/remaining object\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"description\": \"String or object to split\",\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n ]\n },\n \"source\": {\n \"description\": \"Template expression for source\",\n \"type\": \"string\"\n },\n \"by\": {\n \"description\": \"Delimiter for string splitting\",\n \"default\": \",\",\n \"type\": \"string\"\n },\n \"delimiter\": {\n \"description\": \"Alias for by\",\n \"type\": \"string\"\n },\n \"limit\": {\n \"description\": \"Max number of splits\",\n \"type\": \"number\"\n },\n \"keys\": {\n \"description\": \"Keys to extract from object\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"anyOf\": [\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"extracted\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"remaining\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"extracted\",\n \"remaining\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"description\": \"Split result - array or extracted/remaining object\"\n}\n```\n\n\n#### `transform.merge` — Merge Inputs\n\nMerge multiple named inputs into a single output\n\n**Behavior and examples:** `eigenpal docs read steps/transform/merge`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `preservePortNames` | boolean | no | `true` | |\n| `outputKey` | string | no | | |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `items` | array<unknown> | yes | | |\n| `count` | number | yes | | |\n| `merged` | record<string, unknown> | no | | |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"preservePortNames\": {\n \"default\": true,\n \"type\": \"boolean\"\n },\n \"outputKey\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"items\": {\n \"type\": \"array\",\n \"items\": {}\n },\n \"count\": {\n \"type\": \"number\"\n },\n \"merged\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"items\",\n \"count\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.template` — Fill Template\n\nFill a DOCX or XLSX workspace template with data from previous steps. Reference a tmpl_... id, or a local ./templates/file.xlsx path (CLI push uploads it). Git agent templates use the fill-template skill instead and cannot be referenced here. File IDs (file_...) are not accepted. Placeholders are auto-detected. In the Office file use {placeholder} and {table:array.prop}; {{placeholder}} in an XLSX file is rejected ({{ }} is YAML Liquid only).\n\n**Behavior and examples:** `eigenpal docs read steps/transform/template`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `templateId` | string | no | | Workspace template ID (tmpl_...) from the Templates table (DOCX or XLSX). Git agent templates cannot be referenced here. File IDs (file_...) are not accepted. Mutually exclusive with `template`. |\n| `template` | string | no | | Local DOCX or XLSX path relative to the workflow YAML (e.g. ./templates/foo.xlsx). CLI push keeps the real path inside the workflow project unless you pass --allow-external-templates, uploads unmatched files as new tmpl_ resources, and does not rewrite the source YAML. Mutually exclusive with templateId. |\n| `templateRevisionId` | string | no | | Immutable template revision ID (tmpr_...). Pin this when executions must keep using the same template bytes after replacements. Requires templateId; local `template` paths are pinned automatically on push. |\n| `data` | record<string, unknown> | yes | | Data object to merge into template. Each key must be explicitly defined - cannot pass a whole object as single expression. |\n| `outputFilename` | string | no | | Output filename - supports {{field}} syntax; .docx or .xlsx extension is added if omitted |\n| `highlightNotFound` | boolean | no | `true` | Highlight missing variables with red-colored text in the output document (DOCX only; ignored for XLSX) |\n| `notFoundText` | string | no | `\"NOT FOUND\"` | Text to display for missing variables when highlightNotFound is enabled (DOCX only; ignored for XLSX) |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `fileId` | string | yes | | File ID from files table |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"templateId\": {\n \"description\": \"Workspace template ID (tmpl_...) from the Templates table (DOCX or XLSX). Git agent templates cannot be referenced here. File IDs (file_...) are not accepted. Mutually exclusive with `template`.\",\n \"type\": \"string\",\n \"pattern\": \"^tmpl_[A-Za-z0-9_-]{21}$\"\n },\n \"template\": {\n \"description\": \"Local DOCX or XLSX path relative to the workflow YAML (e.g. ./templates/foo.xlsx). CLI push keeps the real path inside the workflow project unless you pass --allow-external-templates, uploads unmatched files as new tmpl_ resources, and does not rewrite the source YAML. Mutually exclusive with templateId.\",\n \"type\": \"string\"\n },\n \"templateRevisionId\": {\n \"description\": \"Immutable template revision ID (tmpr_...). Pin this when executions must keep using the same template bytes after replacements. Requires templateId; local `template` paths are pinned automatically on push.\",\n \"type\": \"string\",\n \"pattern\": \"^tmpr_[A-Za-z0-9_-]{21}$\"\n },\n \"data\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Data object to merge into template. Each key must be explicitly defined - cannot pass a whole object as single expression.\"\n },\n \"outputFilename\": {\n \"description\": \"Output filename - supports {{field}} syntax; .docx or .xlsx extension is added if omitted\",\n \"type\": \"string\"\n },\n \"highlightNotFound\": {\n \"description\": \"Highlight missing variables with red-colored text in the output document (DOCX only; ignored for XLSX)\",\n \"default\": true,\n \"type\": \"boolean\"\n },\n \"notFoundText\": {\n \"description\": \"Text to display for missing variables when highlightNotFound is enabled (DOCX only; ignored for XLSX)\",\n \"default\": \"NOT FOUND\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"data\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"fileId\": {\n \"type\": \"string\",\n \"description\": \"File ID from files table\"\n }\n },\n \"required\": [\n \"fileId\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.pdf-embed` — Embed PDF Text\n\nEmbed OCR text layer into scanned PDFs/images to make them searchable\n\n**Behavior and examples:** `eigenpal docs read steps/transform/pdf-embed`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | File input - template expression e.g. {{input.document}} |\n| `parseResult` | string | yes | | Parse result - template expression e.g. {{steps.parse.output}} |\n| `outputFilename` | string | no | | Output filename - supports {{filename}} syntax |\n| `confidenceThreshold` | number | no | `0.7` | Minimum OCR confidence (0-1) to include a word |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `fileId` | string | yes | | File ID from files table |\n| `pageCount` | number | yes | | Number of pages in the output PDF |\n| `wordCount` | number | yes | | Number of words embedded |\n| `text` | string | yes | | Extracted text from the document |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"File input - template expression e.g. {{input.document}}\"\n },\n \"parseResult\": {\n \"type\": \"string\",\n \"description\": \"Parse result - template expression e.g. {{steps.parse.output}}\"\n },\n \"outputFilename\": {\n \"description\": \"Output filename - supports {{filename}} syntax\",\n \"type\": \"string\"\n },\n \"confidenceThreshold\": {\n \"description\": \"Minimum OCR confidence (0-1) to include a word\",\n \"default\": 0.7,\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n }\n },\n \"required\": [\n \"input\",\n \"parseResult\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"fileId\": {\n \"type\": \"string\",\n \"description\": \"File ID from files table\"\n },\n \"pageCount\": {\n \"type\": \"number\",\n \"description\": \"Number of pages in the output PDF\"\n },\n \"wordCount\": {\n \"type\": \"number\",\n \"description\": \"Number of words embedded\"\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Extracted text from the document\"\n }\n },\n \"required\": [\n \"fileId\",\n \"pageCount\",\n \"wordCount\",\n \"text\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.xlsx-to-json` — Spreadsheet to JSON\n\nConvert an XLS or XLSX spreadsheet to a JSON array of row objects. Supports headerless files, named or positional columns, displayed text, and a single rectangular range.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/xlsx-to-json`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | File input - template expression e.g. {{input.document}} resolving to a scoped $file artifact at runtime |\n| `sheet` | integer \\| string | no | | Sheet to read: 0-based index or exact sheet name. Omit for the first sheet. |\n| `outputCsv` | boolean | no | `false` | If true, also write CSV to storage and include fileId. Zero-config uses the historical full-sheet SheetJS CSV. When columns, range, headerRow, valueMode, blankCells, or blankRows are set, CSV matches that projection. |\n| `includeMetadata` | boolean | no | `false` | If true, include sheet metadata and diagnostics in the step output. Omit to keep output as rows (and fileId when outputCsv is true). Warnings are still logged when this is false. |\n| `outputFilename` | string | no | | Output CSV filename when outputCsv is true - supports LiquidJS e.g. {{filename}}.csv |\n| `headerRow` | `false` \\| integer | no | | Header row: a positive 1-based Excel row, or false to keep the first effective row as data. Omit to use the first effective row as the header. When range is set and this is omitted, the first range row is the header. |\n| `columns` | array<object> | no | | Ordered output columns. Each item needs a key and exactly one source: index (0-based absolute column) or header (exact displayed header text). Named header sources require a header row. Omit to keep every column in the effective range. |\n| `valueMode` | `\"raw\"` \\| `\"displayed\"` | no | `\"raw\"` | raw (default) returns typed cached cell values. displayed returns formatted cell text (dates, leading zeros, punctuation, diacritics, embedded newlines). Formulas are never calculated. |\n| `range` | string | no | | Optional rectangular A1 range without a sheet qualifier, e.g. A1:D20. Disjoint ranges are rejected. |\n| `blankCells` | `\"empty-string\"` \\| `\"null\"` \\| `\"omit\"` | no | `\"empty-string\"` | How truly empty cells appear in each row object. Default empty-string. 0, false, and a formula that cached an empty string are not empty cells. |\n| `blankRows` | `\"skip\"` \\| `\"keep\"` | no | `\"skip\"` | skip (default) drops rows whose projected columns are all truly empty. keep retains them. Detection uses projected columns only. |\n| `limits` | object | no | | Optional workload caps that can only lower the server defaults. Omitted fields use the server defaults. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `rows` | array<record<string, unknown>> | yes | | Array of row objects (first row = headers as keys unless headerRow is false) |\n| `fileId` | string | no | | File ID of stored CSV when outputCsv is true |\n| `sheet` | object | no | | Selected sheet metadata after projection. Present only when includeMetadata is true. |\n| `diagnostics` | array<object> | no | | Non-fatal warnings collected while reading the sheet. Present only when includeMetadata is true. Warnings are still logged when metadata is omitted. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"File input - template expression e.g. {{input.document}} resolving to a scoped $file artifact at runtime\"\n },\n \"sheet\": {\n \"description\": \"Sheet to read: 0-based index or exact sheet name. Omit for the first sheet.\",\n \"anyOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"outputCsv\": {\n \"description\": \"If true, also write CSV to storage and include fileId. Zero-config uses the historical full-sheet SheetJS CSV. When columns, range, headerRow, valueMode, blankCells, or blankRows are set, CSV matches that projection.\",\n \"default\": false,\n \"type\": \"boolean\"\n },\n \"includeMetadata\": {\n \"description\": \"If true, include sheet metadata and diagnostics in the step output. Omit to keep output as rows (and fileId when outputCsv is true). Warnings are still logged when this is false.\",\n \"default\": false,\n \"type\": \"boolean\"\n },\n \"outputFilename\": {\n \"description\": \"Output CSV filename when outputCsv is true - supports LiquidJS e.g. {{filename}}.csv\",\n \"type\": \"string\"\n },\n \"headerRow\": {\n \"description\": \"Header row: a positive 1-based Excel row, or false to keep the first effective row as data. Omit to use the first effective row as the header. When range is set and this is omitted, the first range row is the header.\",\n \"anyOf\": [\n {\n \"type\": \"boolean\",\n \"const\": false\n },\n {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991\n }\n ]\n },\n \"columns\": {\n \"description\": \"Ordered output columns. Each item needs a key and exactly one source: index (0-based absolute column) or header (exact displayed header text). Named header sources require a header row. Omit to keep every column in the effective range.\",\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"key\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Output object key for this column\"\n },\n \"index\": {\n \"description\": \"0-based absolute worksheet column. Use instead of header.\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"header\": {\n \"description\": \"Exact displayed header text. Use instead of index. Requires a header row.\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"occurrence\": {\n \"description\": \"1-based occurrence of header when the same displayed header appears more than once. Only valid with header.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"key\"\n ],\n \"additionalProperties\": false\n }\n },\n \"valueMode\": {\n \"description\": \"raw (default) returns typed cached cell values. displayed returns formatted cell text (dates, leading zeros, punctuation, diacritics, embedded newlines). Formulas are never calculated.\",\n \"default\": \"raw\",\n \"type\": \"string\",\n \"enum\": [\n \"raw\",\n \"displayed\"\n ]\n },\n \"range\": {\n \"description\": \"Optional rectangular A1 range without a sheet qualifier, e.g. A1:D20. Disjoint ranges are rejected.\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"blankCells\": {\n \"description\": \"How truly empty cells appear in each row object. Default empty-string. 0, false, and a formula that cached an empty string are not empty cells.\",\n \"default\": \"empty-string\",\n \"type\": \"string\",\n \"enum\": [\n \"empty-string\",\n \"null\",\n \"omit\"\n ]\n },\n \"blankRows\": {\n \"description\": \"skip (default) drops rows whose projected columns are all truly empty. keep retains them. Detection uses projected columns only.\",\n \"default\": \"skip\",\n \"type\": \"string\",\n \"enum\": [\n \"skip\",\n \"keep\"\n ]\n },\n \"limits\": {\n \"type\": \"object\",\n \"properties\": {\n \"maxInputBytes\": {\n \"description\": \"Maximum input file size in bytes. Omitted uses the server default (52,428,800, 50 MiB). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 52428800\n },\n \"maxRows\": {\n \"description\": \"Maximum rows in the selected range. Omitted uses the server default (100,000). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 100000\n },\n \"maxColumns\": {\n \"description\": \"Maximum columns in the selected range. Omitted uses the server default (256). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 256\n },\n \"maxCells\": {\n \"description\": \"Maximum cells in the selected range. Omitted uses the server default (5,000,000). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 5000000\n }\n },\n \"additionalProperties\": false,\n \"description\": \"Optional workload caps that can only lower the server defaults. Omitted fields use the server defaults.\"\n }\n },\n \"required\": [\n \"input\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"rows\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"description\": \"Array of row objects (first row = headers as keys unless headerRow is false)\"\n },\n \"fileId\": {\n \"description\": \"File ID of stored CSV when outputCsv is true\",\n \"type\": \"string\"\n },\n \"sheet\": {\n \"description\": \"Selected sheet metadata after projection. Present only when includeMetadata is true.\",\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Selected sheet name\"\n },\n \"index\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"0-based sheet index in the workbook\"\n },\n \"range\": {\n \"type\": \"string\",\n \"description\": \"Effective rectangular A1 range that was read\"\n },\n \"rowCount\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Number of data rows returned in rows\"\n },\n \"columns\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Output keys in order\"\n }\n },\n \"required\": [\n \"name\",\n \"index\",\n \"range\",\n \"rowCount\",\n \"columns\"\n ],\n \"additionalProperties\": false\n },\n \"diagnostics\": {\n \"description\": \"Non-fatal warnings collected while reading the sheet. Present only when includeMetadata is true. Warnings are still logged when metadata is omitted.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"severity\": {\n \"type\": \"string\",\n \"const\": \"warning\",\n \"description\": \"Diagnostic severity\"\n },\n \"code\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Stable diagnostic code\"\n },\n \"message\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Human-readable diagnostic message\"\n },\n \"cell\": {\n \"description\": \"A1 address of the related cell when applicable\",\n \"type\": \"string\"\n },\n \"row\": {\n \"description\": \"1-based Excel row when applicable\",\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"column\": {\n \"description\": \"0-based worksheet column when applicable\",\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"severity\",\n \"code\",\n \"message\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"rows\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.json-to-xlsx` — JSON to XLSX\n\nConvert JSON rows into an XLSX spreadsheet. Pair with transform.xlsx-to-json. Formula-looking strings stay text; nested cell values are rejected.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/json-to-xlsx`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `filename` | string | no | | Output filename — supports LiquidJS; .xlsx is added if omitted |\n| `columns` | array<object> | no | | Ordered columns for a single-sheet workbook. Use sheets for multiple sheets. |\n| `rows` | string \\| array<record<string, unknown>> | no | | Array of row objects, or a template expression that resolves to one |\n| `sheets` | array<object> | no | | Multiple sheets. Do not combine with top-level columns/rows. |\n| `limits` | object | no | | Optional workload caps that can only lower the server defaults. Omitted fields use the server defaults. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `fileId` | string | yes | | File ID from the files table |\n| `filename` | string | yes | | Sanitized output filename including .xlsx |\n| `sheetCount` | integer | yes | | Number of sheets in the workbook |\n| `sheets` | array<object> | yes | | Per-sheet name and row count |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"filename\": {\n \"description\": \"Output filename — supports LiquidJS; .xlsx is added if omitted\",\n \"type\": \"string\"\n },\n \"columns\": {\n \"description\": \"Ordered columns for a single-sheet workbook. Use sheets for multiple sheets.\",\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"key\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Row object key to read for this column\"\n },\n \"header\": {\n \"description\": \"Header cell text. Defaults to key.\",\n \"type\": \"string\"\n },\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"string\",\n \"number\",\n \"boolean\",\n \"date\"\n ],\n \"description\": \"Cell type. Omit to infer from the JSON value. YYYY-MM-DD strings stay text unless type is date.\"\n }\n },\n \"required\": [\n \"key\"\n ],\n \"additionalProperties\": false\n }\n },\n \"rows\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Template expression resolving to an array of row objects, e.g. {{ steps.extract.output.items }}\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"description\": \"Array of row objects\"\n }\n ],\n \"description\": \"Array of row objects, or a template expression that resolves to one\"\n },\n \"sheets\": {\n \"description\": \"Multiple sheets. Do not combine with top-level columns/rows.\",\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Sheet name. Defaults to Sheet1, Sheet2, …. Excel limit: 31 characters. Cannot contain : \\\\ / ? * [ ], cannot start or end with a single quote, and History is reserved.\"\n },\n \"columns\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"key\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Row object key to read for this column\"\n },\n \"header\": {\n \"description\": \"Header cell text. Defaults to key.\",\n \"type\": \"string\"\n },\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"string\",\n \"number\",\n \"boolean\",\n \"date\"\n ],\n \"description\": \"Cell type. Omit to infer from the JSON value. YYYY-MM-DD strings stay text unless type is date.\"\n }\n },\n \"required\": [\n \"key\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Ordered columns for this sheet\"\n },\n \"rows\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Template expression resolving to an array of row objects, e.g. {{ steps.extract.output.items }}\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"description\": \"Array of row objects\"\n }\n ],\n \"description\": \"Array of row objects, or a template expression that resolves to one\"\n }\n },\n \"required\": [\n \"columns\",\n \"rows\"\n ],\n \"additionalProperties\": false\n }\n },\n \"limits\": {\n \"type\": \"object\",\n \"properties\": {\n \"maxSheets\": {\n \"description\": \"Maximum worksheets. Omitted uses the server default (50). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 50\n },\n \"maxRowsPerSheet\": {\n \"description\": \"Maximum data rows per sheet. Omitted uses the server default (100,000). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 100000\n },\n \"maxTotalRows\": {\n \"description\": \"Maximum data rows summed across all sheets. Omitted uses the server default (250,000). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 250000\n },\n \"maxColumns\": {\n \"description\": \"Maximum columns per sheet. Omitted uses the server default (256). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 256\n },\n \"maxTotalCells\": {\n \"description\": \"Maximum populated cells across the workbook. Omitted uses the server default (5,000,000). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 5000000\n },\n \"maxOutputBytes\": {\n \"description\": \"Maximum serialized .xlsx size in bytes. Omitted uses the server default (52,428,800, 50 MiB). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 52428800\n }\n },\n \"additionalProperties\": false,\n \"description\": \"Optional workload caps that can only lower the server defaults. Omitted fields use the server defaults.\"\n }\n },\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"fileId\": {\n \"type\": \"string\",\n \"description\": \"File ID from the files table\"\n },\n \"filename\": {\n \"type\": \"string\",\n \"description\": \"Sanitized output filename including .xlsx\"\n },\n \"sheetCount\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991,\n \"description\": \"Number of sheets in the workbook\"\n },\n \"sheets\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Written sheet name\"\n },\n \"rowCount\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Number of data rows written (excludes header)\"\n }\n },\n \"required\": [\n \"name\",\n \"rowCount\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Per-sheet name and row count\"\n }\n },\n \"required\": [\n \"fileId\",\n \"filename\",\n \"sheetCount\",\n \"sheets\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.script` — Script\n\nExecute a TypeScript function in a QuickJS sandbox. Input keys become function parameters in any order, and the required `: R` return-type annotation IS this step's output schema: `inputs: { items, taxRate }` accepts `function script(taxRate: …, items: …): R { … }`.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/script`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `inputs` | record<string, string> | no | | Named inputs mapped from template expressions. Keys become function parameters; parameter order is independent of YAML/JSON key order: `inputs: { items, taxRate }` accepts `function script(taxRate: …, items: …): R { … }`. |\n| `function` | string | yes | | TypeScript function declaration. Must be `function script(args): R { … }` where parameter names exactly match the configured input keys in any order and `R` is a return type annotation. The annotation IS this step's output schema. |\n| `timeout` | number | no | `5000` | Max execution time in milliseconds (default: 5000) |\n| `memoryLimit` | number | no | `10485760` | Max memory in bytes (default: 10MB) |\n\n**Output:** `unknown`\n\n> Value returned from script. Validated at runtime against the JSON Schema derived from the function's return type annotation.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"inputs\": {\n \"description\": \"Named inputs mapped from template expressions. Keys become function parameters; parameter order is independent of YAML/JSON key order: `inputs: { items, taxRate }` accepts `function script(taxRate: …, items: …): R { … }`.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"string\"\n }\n },\n \"function\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 50000,\n \"description\": \"TypeScript function declaration. Must be `function script(args): R { … }` where parameter names exactly match the configured input keys in any order and `R` is a return type annotation. The annotation IS this step's output schema.\"\n },\n \"timeout\": {\n \"description\": \"Max execution time in milliseconds (default: 5000)\",\n \"default\": 5000,\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"memoryLimit\": {\n \"description\": \"Max memory in bytes (default: 10MB)\",\n \"default\": 10485760,\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n }\n },\n \"required\": [\n \"function\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"description\": \"Value returned from script. Validated at runtime against the JSON Schema derived from the function's return type annotation.\"\n}\n```\n\n\n#### `transform.text-chunker` — Text Chunker\n\nSplit long text into chunks with regex-anchored boundaries, overlap, and header preservation. Accepts raw text or a parsed-document object; chunks carry source page indexes when pages are provided.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/text-chunker`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string \\| record<string, unknown> | yes | | Either raw text or a parsed-document object `{ pages: [{ pageIndex, text }] }` (e.g. `{{ steps.parse.output }}`). Pages preserve per-chunk page provenance. |\n| `maxChars` | integer | yes | | Target chunk size in characters. Hard ceiling per chunk is 1.5×. |\n| `overlap` | integer | no | `0` | Characters duplicated at chunk boundaries (default 0). Must be < maxChars / 2. |\n| `splitOn` | array<string> | no | | Ordered list of regexes; the first that matches near the chunk boundary wins. Falls back to char-cut when none match. Tip: list narrowest first (e.g. /\\d+\\.\\d+\\s+/ before /\\n\\n+/). |\n| `maxChunks` | integer | no | `64` | Safety cap; later chunks are dropped and `summary.truncated` flips to true. |\n| `preserveHeader` | integer | no | `0` | Prepend the first N characters of the input to every chunk (good for \"always include the contract title\"). |\n| `minChunkChars` | integer | no | `0` | Trailing chunks shorter than this are merged into the previous chunk. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `chunks` | array<object> | yes | | |\n| `summary` | object | yes | | |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"pages\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"text\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"pageIndex\",\n \"text\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"pages\"\n ],\n \"additionalProperties\": {}\n }\n ],\n \"description\": \"Either raw text or a parsed-document object `{ pages: [{ pageIndex, text }] }` (e.g. `{{ steps.parse.output }}`). Pages preserve per-chunk page provenance.\"\n },\n \"maxChars\": {\n \"type\": \"integer\",\n \"minimum\": 100,\n \"maximum\": 100000,\n \"description\": \"Target chunk size in characters. Hard ceiling per chunk is 1.5×.\"\n },\n \"overlap\": {\n \"default\": 0,\n \"description\": \"Characters duplicated at chunk boundaries (default 0). Must be < maxChars / 2.\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"splitOn\": {\n \"description\": \"Ordered list of regexes; the first that matches near the chunk boundary wins. Falls back to char-cut when none match. Tip: list narrowest first (e.g. /\\\\d+\\\\.\\\\d+\\\\s+/ before /\\\\n\\\\n+/).\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"maxChunks\": {\n \"default\": 64,\n \"description\": \"Safety cap; later chunks are dropped and `summary.truncated` flips to true.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 1000\n },\n \"preserveHeader\": {\n \"default\": 0,\n \"description\": \"Prepend the first N characters of the input to every chunk (good for \\\"always include the contract title\\\").\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"minChunkChars\": {\n \"default\": 0,\n \"description\": \"Trailing chunks shorter than this are merged into the previous chunk.\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"input\",\n \"maxChars\",\n \"overlap\",\n \"maxChunks\",\n \"preserveHeader\",\n \"minChunkChars\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"chunks\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"text\": {\n \"type\": \"string\"\n },\n \"index\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"total\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"startOffset\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"endOffset\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Page indexes that contributed text to this chunk (empty for raw-string input).\"\n }\n },\n \"required\": [\n \"text\",\n \"index\",\n \"total\",\n \"startOffset\",\n \"endOffset\",\n \"pages\"\n ],\n \"additionalProperties\": false\n }\n },\n \"summary\": {\n \"type\": \"object\",\n \"properties\": {\n \"inputChars\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"totalChunks\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"avgChunkChars\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"maxChunkChars\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"truncated\": {\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"inputChars\",\n \"totalChunks\",\n \"avgChunkChars\",\n \"maxChunkChars\",\n \"truncated\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"chunks\",\n \"summary\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.regex-extract` — Regex Extract\n\nPull named fields from text via regex patterns (deterministic counterpart to ai.extract). Accepts raw text or a parsed-document object; matches carry `_evidence.pageIndex` when pages are provided.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/regex-extract`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string \\| record<string, unknown> | yes | | Either raw text or a parsed-document object `{ pages: [{ pageIndex, text }] }`. Pages enable per-match `_evidence.pageIndex`. |\n| `fields` | record<string, object> | yes | | Named field → pattern mapping. |\n| `flags` | string | no | | Default regex flags applied when a field omits its own `flags`. Subset of \"gimsuy\". |\n| `searchWindow` | integer | no | | Only search the first N characters of input (perf). Omit for full search. |\n\n**Output:** `record<string, unknown>`\n\n> Field name → extracted value (or default), plus `_evidence: { [field]: { pageIndex, matchOffset, raw } }` and `_unmatched: string[]`.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"pages\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"text\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"pageIndex\",\n \"text\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"pages\"\n ],\n \"additionalProperties\": {}\n }\n ],\n \"description\": \"Either raw text or a parsed-document object `{ pages: [{ pageIndex, text }] }`. Pages enable per-match `_evidence.pageIndex`.\"\n },\n \"fields\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"pattern\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Regex source. Must contain at least one capturing group.\"\n },\n \"flags\": {\n \"description\": \"Per-field regex flags (overrides the step-level `flags`). Subset of \\\"gimsuy\\\".\",\n \"type\": \"string\",\n \"pattern\": \"^(?!.*(.).*\\\\1)[gimsuy]*$\"\n },\n \"max\": {\n \"default\": 1,\n \"description\": \"Max matches to keep. Default 1. Use \\\"all\\\" plus `select` to combine multiple hits.\",\n \"anyOf\": [\n {\n \"type\": \"number\",\n \"const\": 1\n },\n {\n \"type\": \"string\",\n \"const\": \"all\"\n },\n {\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 9007199254740991\n }\n ]\n },\n \"select\": {\n \"description\": \"When max ≠ 1: which match to surface. \\\"first\\\" (default), \\\"last\\\", \\\"min\\\", \\\"max\\\", or \\\"all\\\" — `all` returns the full array of matches AND a parallel array of evidence entries.\",\n \"type\": \"string\",\n \"enum\": [\n \"first\",\n \"last\",\n \"min\",\n \"max\",\n \"all\"\n ]\n },\n \"normalize\": {\n \"type\": \"string\",\n \"enum\": [\n \"strip-spaces\",\n \"strip-punct\",\n \"collapse-whitespace\",\n \"lower\",\n \"upper\",\n \"trim\"\n ]\n },\n \"format\": {\n \"description\": \"Post-processing. iso-date accepts DD.MM.YYYY (1 group) or DD/MM/YYYY split across 3 groups → \\\"YYYY-MM-DD\\\".\",\n \"type\": \"string\",\n \"enum\": [\n \"iso-date\",\n \"iso-number\",\n \"lower\",\n \"upper\",\n \"trim\"\n ]\n },\n \"exclude\": {\n \"description\": \"Post-normalize values to skip (known wrong matches).\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"default\": {\n \"description\": \"Value when nothing matches.\",\n \"type\": \"string\"\n },\n \"contextChars\": {\n \"description\": \"When > 0, evidence entries gain `contextBefore` and `contextAfter` snippets of N characters surrounding the match. Useful for UI/debug display.\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 2000\n }\n },\n \"required\": [\n \"pattern\",\n \"max\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Named field → pattern mapping.\"\n },\n \"flags\": {\n \"description\": \"Default regex flags applied when a field omits its own `flags`. Subset of \\\"gimsuy\\\".\",\n \"type\": \"string\",\n \"pattern\": \"^(?!.*(.).*\\\\1)[gimsuy]*$\"\n },\n \"searchWindow\": {\n \"description\": \"Only search the first N characters of input (perf). Omit for full search.\",\n \"type\": \"integer\",\n \"minimum\": 100,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"input\",\n \"fields\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Field name → extracted value (or default), plus `_evidence: { [field]: { pageIndex, matchOffset, raw } }` and `_unmatched: string[]`.\"\n}\n```\n\n\n#### `transform.archive-list` — List Archive\n\nList files inside a ZIP archive. Returns sorted normalized paths with compressed and uncompressed sizes. Optionally filter by folder prefix or substring, then page with offset and limit. ZIP is the only accepted format in this version. Pair with foreach or parallel_map and transform.archive-extract to open individual files.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/archive-list`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `archive` | string | yes | | Template expression or file reference for the ZIP archive to list. ZIP is the only accepted format in this version. |\n| `prefix` | string | no | | Optional folder prefix inside the archive after path normalization (for example invoices or invoices/). |\n| `search` | string | no | | Optional case-insensitive substring match on the normalized archive path. |\n| `offset` | integer | no | | Number of matching entries to skip after sorting. Default 0. |\n| `limit` | integer | no | | Maximum matching entries to return after sorting. Capped at 10000. Omit to return every matching entry up to that cap. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `entries` | array<object> | yes | | Matching files, sorted by normalized path. Directories and junk paths are omitted. |\n| `total` | integer | yes | | Number of matching entries before offset/limit pagination. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"archive\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Template expression or file reference for the ZIP archive to list. ZIP is the only accepted format in this version.\"\n },\n \"prefix\": {\n \"description\": \"Optional folder prefix inside the archive after path normalization (for example invoices or invoices/).\",\n \"type\": \"string\"\n },\n \"search\": {\n \"description\": \"Optional case-insensitive substring match on the normalized archive path.\",\n \"type\": \"string\"\n },\n \"offset\": {\n \"description\": \"Number of matching entries to skip after sorting. Default 0.\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"limit\": {\n \"description\": \"Maximum matching entries to return after sorting. Capped at 10000. Omit to return every matching entry up to that cap.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10000\n }\n },\n \"required\": [\n \"archive\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"entries\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"path\": {\n \"type\": \"string\",\n \"description\": \"Normalized archive-relative path\"\n },\n \"compressedSize\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Declared compressed size in bytes\"\n },\n \"uncompressedSize\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Declared uncompressed size in bytes\"\n }\n },\n \"required\": [\n \"path\",\n \"compressedSize\",\n \"uncompressedSize\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Matching files, sorted by normalized path. Directories and junk paths are omitted.\"\n },\n \"total\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Number of matching entries before offset/limit pagination.\"\n }\n },\n \"required\": [\n \"entries\",\n \"total\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.archive-extract` — Extract Archive File\n\nExtract exactly one file from a ZIP archive and store it as a run output file. Downstream steps can pass {{ steps.extract.output }} into Parse, Vision, or another archive step. Nested ZIPs are stored as ZIP files, not unpacked. Each extraction is capped at 64 MiB. ZIP is the only accepted format in this version.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/archive-extract`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `archive` | string | yes | | Template expression or file reference for the ZIP archive. ZIP is the only accepted format in this version. |\n| `path` | string | yes | | Archive-relative path of the single file to extract, for example {{ item.path }} from a prior archive-list step. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `fileId` | string | yes | | File ID from the files table |\n| `path` | string | yes | | Normalized archive-relative path that was extracted |\n| `filename` | string | yes | | Basename of the extracted file |\n| `mimeType` | string | yes | | Detected MIME type of the extracted file |\n| `size` | integer | yes | | Uncompressed size in bytes |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"archive\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Template expression or file reference for the ZIP archive. ZIP is the only accepted format in this version.\"\n },\n \"path\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Archive-relative path of the single file to extract, for example {{ item.path }} from a prior archive-list step.\"\n }\n },\n \"required\": [\n \"archive\",\n \"path\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"fileId\": {\n \"type\": \"string\",\n \"description\": \"File ID from the files table\"\n },\n \"path\": {\n \"type\": \"string\",\n \"description\": \"Normalized archive-relative path that was extracted\"\n },\n \"filename\": {\n \"type\": \"string\",\n \"description\": \"Basename of the extracted file\"\n },\n \"mimeType\": {\n \"type\": \"string\",\n \"description\": \"Detected MIME type of the extracted file\"\n },\n \"size\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Uncompressed size in bytes\"\n }\n },\n \"required\": [\n \"fileId\",\n \"path\",\n \"filename\",\n \"mimeType\",\n \"size\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n### Action steps — external side effects\n\n#### `action.http` — HTTP Request\n\nMake an HTTP request to an external API\n\n**Behavior and examples:** `eigenpal docs read steps/action/http`\n\n**Durable retry:** HTTP `GET` and `HEAD` can durably retry transient timeout, rate limits, selected retryable server failures failures. Other methods are not replayed.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `url` | string | yes | | Request URL (supports template expressions) |\n| `method` | `\"GET\"` \\| `\"HEAD\"` \\| `\"POST\"` \\| `\"PUT\"` \\| `\"PATCH\"` \\| `\"DELETE\"` | no | `\"GET\"` | |\n| `headers` | record<string, string> | no | | HTTP headers |\n| `body` | unknown | no | | Request body (JSON or string) |\n| `timeout` | number | no | `30000` | Timeout in milliseconds |\n| `insecureSkipTlsVerify` | boolean | no | `false` | If true, skip TLS certificate verification (use only for read-only public endpoints with bad/expired certs) |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `status` | number | yes | | HTTP status code |\n| `statusText` | string | yes | | |\n| `headers` | record<string, string> | yes | | |\n| `body` | unknown | yes | | Response body (parsed JSON or string) |\n| `responseCharset` | string | yes | | Charset used to decode the response body (e.g. utf-8, windows-1250) |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"url\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Request URL (supports template expressions)\"\n },\n \"method\": {\n \"default\": \"GET\",\n \"type\": \"string\",\n \"enum\": [\n \"GET\",\n \"HEAD\",\n \"POST\",\n \"PUT\",\n \"PATCH\",\n \"DELETE\"\n ]\n },\n \"headers\": {\n \"description\": \"HTTP headers\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"string\"\n }\n },\n \"body\": {\n \"description\": \"Request body (JSON or string)\"\n },\n \"timeout\": {\n \"description\": \"Timeout in milliseconds\",\n \"default\": 30000,\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"insecureSkipTlsVerify\": {\n \"description\": \"If true, skip TLS certificate verification (use only for read-only public endpoints with bad/expired certs)\",\n \"default\": false,\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"url\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"number\",\n \"description\": \"HTTP status code\"\n },\n \"statusText\": {\n \"type\": \"string\"\n },\n \"headers\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"string\"\n }\n },\n \"body\": {\n \"description\": \"Response body (parsed JSON or string)\"\n },\n \"responseCharset\": {\n \"type\": \"string\",\n \"description\": \"Charset used to decode the response body (e.g. utf-8, windows-1250)\"\n }\n },\n \"required\": [\n \"status\",\n \"statusText\",\n \"headers\",\n \"body\",\n \"responseCharset\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `action.invoke-workflow` — Invoke Workflow\n\nExecute another workflow and return its output\n\n**Behavior and examples:** `eigenpal docs read steps/action/invoke-workflow`\n\n**Durable retry:** Invoked workflows are not replayed as durable leaf attempts.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `workflow` | string | no | | Workflow to invoke — definition name or wf_ id (tenant-scoped) |\n| `workflowId` | string | no | | Legacy alias for workflow when the value is a wf_ id |\n| `execution` | `\"inline\"` \\| `\"child\"` | no | | inline: run target steps in this execution (default). child: spawn a separate run with lineage. |\n| `input` | record<string, unknown> | no | | Input record keyed by the invoked workflow's declared inputs |\n| `wait` | boolean | no | | Child mode only. Wait for the invoked workflow to complete and return its output (default: true). Set false for fire-and-forget. |\n| `timeout` | number | no | | Child mode only. Max wait time in ms while the child is queued or running (default: 300000). Once the child pauses for human review, the parent pauses durably instead of using this timeout. |\n| `pollInterval` | number | no | | Child mode only. How often to poll status in ms when waiting (default: 1000) |\n\n**Output:** `record<string, unknown>`\n\n> When wait is true, the invoked workflow's declared output fields are flattened to the top level alongside a `files` array — reference them as {{ steps.<invoke>.output.<field> }} (there is no `.data` or `.result` wrapper). When wait is false, execution metadata. CLI authors can run `eigenpal workflow schema <workflow-id>` to inspect resolved fields; Studio builder agents can call `get_workflow_output_schema`.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"workflow\": {\n \"description\": \"Workflow to invoke — definition name or wf_ id (tenant-scoped)\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"workflowId\": {\n \"description\": \"Legacy alias for workflow when the value is a wf_ id\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"execution\": {\n \"description\": \"inline: run target steps in this execution (default). child: spawn a separate run with lineage.\",\n \"type\": \"string\",\n \"enum\": [\n \"inline\",\n \"child\"\n ]\n },\n \"input\": {\n \"description\": \"Input record keyed by the invoked workflow's declared inputs\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"wait\": {\n \"description\": \"Child mode only. Wait for the invoked workflow to complete and return its output (default: true). Set false for fire-and-forget.\",\n \"type\": \"boolean\"\n },\n \"timeout\": {\n \"description\": \"Child mode only. Max wait time in ms while the child is queued or running (default: 300000). Once the child pauses for human review, the parent pauses durably instead of using this timeout.\",\n \"type\": \"number\"\n },\n \"pollInterval\": {\n \"description\": \"Child mode only. How often to poll status in ms when waiting (default: 1000)\",\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"When wait is true, the invoked workflow's declared output fields are flattened to the top level alongside a `files` array — reference them as {{ steps.<invoke>.output.<field> }} (there is no `.data` or `.result` wrapper). When wait is false, execution metadata. CLI authors can run `eigenpal workflow schema <workflow-id>` to inspect resolved fields; Studio builder agents can call `get_workflow_output_schema`.\"\n}\n```\n\n\n#### `action.website-reader` — Website Reader\n\nFetch a webpage and convert content to markdown\n\n**Behavior and examples:** `eigenpal docs read steps/action/website-reader`\n\n**Durable retry:** Supported for transient timeout, rate limits, selected retryable server failures failures.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `url` | string | yes | | Website URL to fetch (supports template expressions) |\n| `timeout` | number | no | `30000` | Timeout in milliseconds |\n| `encoding` | `\"auto\"` \\| `\"utf-8\"` \\| `\"latin1\"` \\| `\"windows-1250\"` \\| `\"windows-1252\"` \\| `\"iso-8859-2\"` | no | `\"auto\"` | Response encoding. Auto detects from Content-Type header and HTML meta tags. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `markdown` | string | yes | | Page content converted to markdown |\n| `title` | string \\| null | yes | | Page title |\n| `excerpt` | string \\| null | yes | | Page excerpt/description |\n| `byline` | string \\| null | yes | | Author information |\n| `siteName` | string \\| null | yes | | Site name |\n| `length` | number | yes | | Content length in characters |\n| `url` | string | yes | | Final URL after redirects |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"url\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Website URL to fetch (supports template expressions)\"\n },\n \"timeout\": {\n \"description\": \"Timeout in milliseconds\",\n \"default\": 30000,\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"encoding\": {\n \"description\": \"Response encoding. Auto detects from Content-Type header and HTML meta tags.\",\n \"default\": \"auto\",\n \"type\": \"string\",\n \"enum\": [\n \"auto\",\n \"utf-8\",\n \"latin1\",\n \"windows-1250\",\n \"windows-1252\",\n \"iso-8859-2\"\n ]\n }\n },\n \"required\": [\n \"url\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"markdown\": {\n \"type\": \"string\",\n \"description\": \"Page content converted to markdown\"\n },\n \"title\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"Page title\"\n },\n \"excerpt\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"Page excerpt/description\"\n },\n \"byline\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"Author information\"\n },\n \"siteName\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"Site name\"\n },\n \"length\": {\n \"type\": \"number\",\n \"description\": \"Content length in characters\"\n },\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Final URL after redirects\"\n }\n },\n \"required\": [\n \"markdown\",\n \"title\",\n \"excerpt\",\n \"byline\",\n \"siteName\",\n \"length\",\n \"url\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `action.email` — Send Email\n\nSend an email with optional attachments via a selected email server. At most 10 recipients combined across to, cc, and bcc.\n\n**Behavior and examples:** `eigenpal docs read steps/action/email`\n\n**Durable retry:** This control step is not retried durably.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `server` | string | yes | | Outbound email server id (`ems_…`) |\n| `to` | string \\| array<string> | yes | | Primary recipient(s). String, comma-separated list, or nonempty string array. Combined with cc and bcc, at most 10 recipients. |\n| `cc` | string \\| array<string> | no | | Carbon-copy recipient(s). String, comma-separated list, or string array. Combined with to and bcc, at most 10 recipients. |\n| `bcc` | string \\| array<string> | no | | Blind carbon-copy recipient(s). String, comma-separated list, or string array. Combined with to and cc, at most 10 recipients. |\n| `replyTo` | string | no | | Reply-To address (supports template expressions) |\n| `subject` | string | yes | | Email subject |\n| `text` | string | yes | | Plain-text email body (max 50KiB) |\n| `attachments` | array<string \\| object> | no | | Optional same-run attachments as { fileId, filename? } or a bare fileId string (12MiB combined) |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `serverId` | string | yes | | Email server that sent (or would have sent) the message |\n| `messageId` | string \\| null | yes | | Provider message id. Null when delivery was skipped (evaluation runs). |\n| `provider` | `\"resend\"` \\| `\"smtp\"` \\| null | yes | | Email provider that sent the message. Null when delivery was skipped. |\n| `to` | array<string> | yes | | Normalized To recipients |\n| `cc` | array<string> | yes | | Normalized Cc recipients |\n| `bcc` | array<string> | yes | | Normalized Bcc recipients |\n| `attachmentCount` | integer | yes | | Number of attachments sent |\n| `totalBytes` | integer | yes | | Total attachment size in bytes |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"server\": {\n \"type\": \"string\",\n \"maxLength\": 128,\n \"pattern\": \"^ems_[A-Za-z0-9_-]+$\",\n \"description\": \"Outbound email server id (`ems_…`)\"\n },\n \"to\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1\n },\n {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n }\n ],\n \"description\": \"Primary recipient(s). String, comma-separated list, or nonempty string array. Combined with cc and bcc, at most 10 recipients.\"\n },\n \"cc\": {\n \"description\": \"Carbon-copy recipient(s). String, comma-separated list, or string array. Combined with to and bcc, at most 10 recipients.\",\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n }\n ]\n },\n \"bcc\": {\n \"description\": \"Blind carbon-copy recipient(s). String, comma-separated list, or string array. Combined with to and cc, at most 10 recipients.\",\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n }\n ]\n },\n \"replyTo\": {\n \"description\": \"Reply-To address (supports template expressions)\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"subject\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Email subject\"\n },\n \"text\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Plain-text email body (max 50KiB)\"\n },\n \"attachments\": {\n \"description\": \"Optional same-run attachments as { fileId, filename? } or a bare fileId string (12MiB combined)\",\n \"maxItems\": 10,\n \"type\": \"array\",\n \"items\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"File id or template expression that resolves to a file id\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"fileId\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"File id or template expression that resolves to a file id\"\n },\n \"filename\": {\n \"description\": \"Optional download filename override\",\n \"type\": \"string\",\n \"minLength\": 1\n }\n },\n \"required\": [\n \"fileId\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n }\n },\n \"required\": [\n \"server\",\n \"to\",\n \"subject\",\n \"text\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"serverId\": {\n \"type\": \"string\",\n \"maxLength\": 128,\n \"pattern\": \"^ems_[A-Za-z0-9_-]+$\",\n \"description\": \"Email server that sent (or would have sent) the message\"\n },\n \"messageId\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"Provider message id. Null when delivery was skipped (evaluation runs).\"\n },\n \"provider\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"enum\": [\n \"resend\",\n \"smtp\"\n ]\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"Email provider that sent the message. Null when delivery was skipped.\"\n },\n \"to\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Normalized To recipients\"\n },\n \"cc\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Normalized Cc recipients\"\n },\n \"bcc\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Normalized Bcc recipients\"\n },\n \"attachmentCount\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Number of attachments sent\"\n },\n \"totalBytes\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Total attachment size in bytes\"\n }\n },\n \"required\": [\n \"serverId\",\n \"messageId\",\n \"provider\",\n \"to\",\n \"cc\",\n \"bcc\",\n \"attachmentCount\",\n \"totalBytes\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n### Control steps — flow control\n\n#### `control.if` — Condition\n\nBranch execution based on a condition expression\n\n**Behavior and examples:** `eigenpal docs read steps/control/if`\n\n**Durable retry:** The control container itself is not retried, but eligible leaves inside its sequential scope may retry durably.\n\n**Config** (at step level):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `condition` | string | yes | | LiquidJS expression that evaluates to boolean |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `condition` | boolean | yes | | Evaluated condition result |\n| `branch` | `\"then\"` \\| `\"else\"` | yes | | Which branch was executed |\n| `result` | unknown | yes | | Output from executed branch |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"condition\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"LiquidJS expression that evaluates to boolean\"\n }\n },\n \"required\": [\n \"condition\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"condition\": {\n \"type\": \"boolean\",\n \"description\": \"Evaluated condition result\"\n },\n \"branch\": {\n \"type\": \"string\",\n \"enum\": [\n \"then\",\n \"else\"\n ],\n \"description\": \"Which branch was executed\"\n },\n \"result\": {\n \"description\": \"Output from executed branch\"\n }\n },\n \"required\": [\n \"condition\",\n \"branch\",\n \"result\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `control.switch` — Switch\n\nMulti-way routing: resolve an expression and run the first case whose value matches (else default). Cleaner than a nested control.if chain for routing an item to one of N pipelines by a discriminator field like a document type.\n\n**Behavior and examples:** `eigenpal docs read steps/control/switch`\n\n**Durable retry:** The control container itself is not retried, but eligible leaves inside its sequential scope may retry durably.\n\n**Config** (at step level):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `on` | string | yes | | Template expression whose resolved value selects the case, e.g. \"{{ doc.type }}\". |\n| `cases` | array<object> | yes | | Ordered cases; the first whose `when` matches runs. Each case has its own `steps`. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `matched` | string \\| number \\| boolean \\| null | yes | | The `when` value that matched, or null when the default (or no) branch ran. |\n| `branch` | `\"case\"` \\| `\"default\"` \\| `\"none\"` | yes | | Which branch executed. |\n| `result` | unknown | yes | | Output from the executed branch (its last step). |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"on\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Template expression whose resolved value selects the case, e.g. \\\"{{ doc.type }}\\\".\"\n },\n \"cases\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"when\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"boolean\"\n }\n ],\n \"description\": \"Value matched (string-compared) against the resolved `on` value.\"\n }\n },\n \"required\": [\n \"when\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Ordered cases; the first whose `when` matches runs. Each case has its own `steps`.\"\n }\n },\n \"required\": [\n \"on\",\n \"cases\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"matched\": {\n \"anyOf\": [\n {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"boolean\"\n }\n ]\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"The `when` value that matched, or null when the default (or no) branch ran.\"\n },\n \"branch\": {\n \"type\": \"string\",\n \"enum\": [\n \"case\",\n \"default\",\n \"none\"\n ],\n \"description\": \"Which branch executed.\"\n },\n \"result\": {\n \"description\": \"Output from the executed branch (its last step).\"\n }\n },\n \"required\": [\n \"matched\",\n \"branch\",\n \"result\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `control.foreach` — For Each\n\nLoop over an array and execute steps for each item\n\n**Behavior and examples:** `eigenpal docs read steps/control/foreach`\n\n**Durable retry:** The control container itself is not retried, but eligible leaves inside its sequential scope may retry durably.\n\n**Config** (at step level):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `items` | string | yes | | Expression resolving to array |\n| `as` | string | yes | | Variable name for current item |\n| `indexAs` | string | no | | Variable name for current index |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `items` | array<unknown> | yes | | Results from each iteration |\n| `count` | number | yes | | Number of completed iterations |\n| `totalIterations` | number | yes | | Total iterations |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Expression resolving to array\"\n },\n \"as\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Variable name for current item\"\n },\n \"indexAs\": {\n \"description\": \"Variable name for current index\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"items\",\n \"as\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"items\": {\n \"type\": \"array\",\n \"items\": {},\n \"description\": \"Results from each iteration\"\n },\n \"count\": {\n \"type\": \"number\",\n \"description\": \"Number of completed iterations\"\n },\n \"totalIterations\": {\n \"type\": \"number\",\n \"description\": \"Total iterations\"\n }\n },\n \"required\": [\n \"items\",\n \"count\",\n \"totalIterations\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `control.parallel_map` — Parallel Map\n\nIterate over an array with concurrent execution up to a limit\n\n**Behavior and examples:** `eigenpal docs read steps/control/parallel_map`\n\n**Durable retry:** The control container and leaves inside its concurrent branches do not retry durably.\n\n**Config** (at step level):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `items` | string | yes | | Expression resolving to array |\n| `as` | string | yes | | Variable name for current item |\n| `indexAs` | string | no | | Variable name for current index |\n| `concurrency` | integer | no | `5` | Maximum concurrent executions (1-50, default 5) |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `items` | array<unknown> | yes | | Results from each iteration (maintains original order) |\n| `count` | number | yes | | Number of completed iterations |\n| `totalIterations` | number | yes | | Total iterations |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Expression resolving to array\"\n },\n \"as\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Variable name for current item\"\n },\n \"indexAs\": {\n \"description\": \"Variable name for current index\",\n \"type\": \"string\"\n },\n \"concurrency\": {\n \"description\": \"Maximum concurrent executions (1-50, default 5)\",\n \"default\": 5,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 50\n }\n },\n \"required\": [\n \"items\",\n \"as\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"items\": {\n \"type\": \"array\",\n \"items\": {},\n \"description\": \"Results from each iteration (maintains original order)\"\n },\n \"count\": {\n \"type\": \"number\",\n \"description\": \"Number of completed iterations\"\n },\n \"totalIterations\": {\n \"type\": \"number\",\n \"description\": \"Total iterations\"\n }\n },\n \"required\": [\n \"items\",\n \"count\",\n \"totalIterations\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `control.parallel` — Parallel\n\nExecute multiple branches concurrently\n\n**Behavior and examples:** `eigenpal docs read steps/control/parallel`\n\n**Durable retry:** The control container and leaves inside its concurrent branches do not retry durably.\n\n**Config** (at step level):\n\n_No fields._\n\n**Output:** `record<string, unknown>`\n\n> Results keyed by branch name\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {},\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Results keyed by branch name\"\n}\n```\n\n\n#### `control.wait` — Wait\n\nPause workflow execution for a specified duration\n\n**Behavior and examples:** `eigenpal docs read steps/control/wait`\n\n**Durable retry:** This control step is not retried durably.\n\n**Config** (at step level):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `duration` | number | yes | | Duration in milliseconds |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `waited` | number | yes | | Actual milliseconds waited |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"duration\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0,\n \"description\": \"Duration in milliseconds\"\n }\n },\n \"required\": [\n \"duration\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"waited\": {\n \"type\": \"number\",\n \"description\": \"Actual milliseconds waited\"\n }\n },\n \"required\": [\n \"waited\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `control.human_review` — Human Review\n\nPause the run for selective field confirmation or correction, then continue with the reviewed data.\n\n**Behavior and examples:** `eigenpal docs read steps/control/human_review`\n\n**Durable retry:** This control step is not retried durably.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `data` | string \\| record<string, unknown> \\| array<unknown> | yes | | Structured data or a template expression resolving to an object or array |\n| `schema` | record<string, unknown> | no | | Optional JSON Schema for validating edits and exposing downstream autocomplete. When omitted with metadataFrom: ai.extract and a direct whole-output data expression, the extract step schema is reused. |\n| `metadataFrom` | `\"ai.extract\"` | no | | Producer adapter. `ai.extract` derives field confidence from `_grounding` and strips that reserved key from review data. Grounding is not stripped unless this is set. |\n| `fieldMetadata` | record<string, object> | no | `{}` | RFC 6901 pointer map of producer facts (labels, confidence, display). Selection policy belongs under selection.fields; fieldMetadata.review is a legacy alias. |\n| `selection` | object | yes | | Field selection policy resolved once when the task is created |\n| `files` | object | no | `{\"includeRunInputs\":true,\"attachments\":[]}` | Run input files and optional current-run attachments for reviewers |\n| `instructions` | string | no | | Free-form guidance shown above the review workspace |\n\n**Output:** `unknown`\n\n> The complete reviewed data in the same object or array shape as the input.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"data\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1\n },\n {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n {\n \"type\": \"array\",\n \"items\": {}\n }\n ],\n \"description\": \"Structured data or a template expression resolving to an object or array\"\n },\n \"schema\": {\n \"description\": \"Optional JSON Schema for validating edits and exposing downstream autocomplete. When omitted with metadataFrom: ai.extract and a direct whole-output data expression, the extract step schema is reused.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"metadataFrom\": {\n \"type\": \"string\",\n \"enum\": [\n \"ai.extract\"\n ],\n \"description\": \"Producer adapter. `ai.extract` derives field confidence from `_grounding` and strips that reserved key from review data. Grounding is not stripped unless this is set.\"\n },\n \"fieldMetadata\": {\n \"description\": \"RFC 6901 pointer map of producer facts (labels, confidence, display). Selection policy belongs under selection.fields; fieldMetadata.review is a legacy alias.\",\n \"default\": {},\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"confidence\": {\n \"anyOf\": [\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n {\n \"type\": \"string\",\n \"minLength\": 1\n }\n ]\n },\n \"label\": {\n \"type\": \"string\",\n \"maxLength\": 500\n },\n \"description\": {\n \"type\": \"string\",\n \"maxLength\": 5000\n },\n \"review\": {\n \"default\": \"auto\",\n \"type\": \"string\",\n \"enum\": [\n \"auto\",\n \"always\",\n \"never\"\n ]\n },\n \"display\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"additionalProperties\": false\n }\n },\n \"selection\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"include\": {\n \"description\": \"RFC 6901 pointers or wildcard patterns (for example `/items/*/amount`)\",\n \"default\": [],\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"maxLength\": 512\n }\n },\n \"exclude\": {\n \"description\": \"RFC 6901 pointers removed after include expansion\",\n \"default\": [],\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"maxLength\": 512\n }\n },\n \"fields\": {\n \"description\": \"Per-field selection policy keyed by exact pointers or wildcard patterns. Exact beats the most specific wildcard; equally specific overlapping wildcards are rejected.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\",\n \"maxLength\": 512\n },\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"review\": {\n \"type\": \"string\",\n \"enum\": [\n \"always\",\n \"never\",\n \"skip\"\n ]\n },\n \"threshold\": {\n \"anyOf\": [\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n {\n \"type\": \"string\",\n \"minLength\": 1\n }\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"confidence\",\n \"description\": \"Review fields below threshold or missing confidence\"\n },\n \"threshold\": {\n \"anyOf\": [\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n {\n \"type\": \"string\",\n \"minLength\": 1\n }\n ],\n \"description\": \"Fields with confidence strictly below this value require review. Equality auto-approves. Numeric 0–1 or categorical low|medium|high. Template strings are resolved at runtime.\"\n },\n \"missingConfidence\": {\n \"description\": \"Whether unscored or type-mismatched fields require review (default review)\",\n \"default\": \"review\",\n \"type\": \"string\",\n \"enum\": [\n \"review\",\n \"skip\"\n ]\n }\n },\n \"required\": [\n \"mode\",\n \"threshold\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"include\": {\n \"description\": \"RFC 6901 pointers or wildcard patterns (for example `/items/*/amount`)\",\n \"default\": [],\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"maxLength\": 512\n }\n },\n \"exclude\": {\n \"description\": \"RFC 6901 pointers removed after include expansion\",\n \"default\": [],\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"maxLength\": 512\n }\n },\n \"fields\": {\n \"description\": \"Per-field selection policy keyed by exact pointers or wildcard patterns. Exact beats the most specific wildcard; equally specific overlapping wildcards are rejected.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\",\n \"maxLength\": 512\n },\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"review\": {\n \"type\": \"string\",\n \"enum\": [\n \"always\",\n \"never\",\n \"skip\"\n ]\n },\n \"threshold\": {\n \"anyOf\": [\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n {\n \"type\": \"string\",\n \"minLength\": 1\n }\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"explicit\",\n \"description\": \"Review only paths matched by include/exclude\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"include\": {\n \"description\": \"RFC 6901 pointers or wildcard patterns (for example `/items/*/amount`)\",\n \"default\": [],\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"maxLength\": 512\n }\n },\n \"exclude\": {\n \"description\": \"RFC 6901 pointers removed after include expansion\",\n \"default\": [],\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"maxLength\": 512\n }\n },\n \"fields\": {\n \"description\": \"Per-field selection policy keyed by exact pointers or wildcard patterns. Exact beats the most specific wildcard; equally specific overlapping wildcards are rejected.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\",\n \"maxLength\": 512\n },\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"review\": {\n \"type\": \"string\",\n \"enum\": [\n \"always\",\n \"never\",\n \"skip\"\n ]\n },\n \"threshold\": {\n \"anyOf\": [\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n {\n \"type\": \"string\",\n \"minLength\": 1\n }\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"all\",\n \"description\": \"Review every scalar leaf unless excluded or review: never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"description\": \"Field selection policy resolved once when the task is created\"\n },\n \"files\": {\n \"description\": \"Run input files and optional current-run attachments for reviewers\",\n \"default\": {\n \"includeRunInputs\": true,\n \"attachments\": []\n },\n \"type\": \"object\",\n \"properties\": {\n \"includeRunInputs\": {\n \"description\": \"Attach every authorized run input file to the review task\",\n \"default\": true,\n \"type\": \"boolean\"\n },\n \"attachments\": {\n \"description\": \"Additional current-run file or artifact template expressions\",\n \"default\": [],\n \"maxItems\": 100,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n }\n },\n \"additionalProperties\": false\n },\n \"instructions\": {\n \"description\": \"Free-form guidance shown above the review workspace\",\n \"type\": \"string\",\n \"maxLength\": 10000\n }\n },\n \"required\": [\n \"data\",\n \"selection\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"description\": \"The complete reviewed data in the same object or array shape as the input.\"\n}\n```\n\n\n#### `control.fail` — Fail\n\nTerminate the workflow with a typed status code + message. With an optional condition, only fails when the condition is truthy; otherwise always fails when reached. Pair with ai.classify or any prior step to fail fast on bad inputs.\n\n**Behavior and examples:** `eigenpal docs read steps/control/fail`\n\n**Durable retry:** This control step is not retried durably.\n\n**Config** (at step level):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `condition` | string | no | | Optional LiquidJS expression. When set, the step only fails if this evaluates truthy; when omitted, it always fails when reached (compose with control.if for legacy gating). |\n| `statusCode` | integer | no | `422` | HTTP-style status code returned to the caller (sync runs) and persisted on the execution. Default 422 (Unprocessable Entity). |\n| `message` | string | yes | | Human-readable failure message. Supports template expressions, e.g. \"Document classified as {{ steps.classify.output.label }}\". |\n\n**Output:** `unknown`\n\n> control.fail never produces output — it terminates the workflow when triggered.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"condition\": {\n \"description\": \"Optional LiquidJS expression. When set, the step only fails if this evaluates truthy; when omitted, it always fails when reached (compose with control.if for legacy gating).\",\n \"type\": \"string\"\n },\n \"statusCode\": {\n \"default\": 422,\n \"description\": \"HTTP-style status code returned to the caller (sync runs) and persisted on the execution. Default 422 (Unprocessable Entity).\",\n \"type\": \"integer\",\n \"minimum\": 400,\n \"maximum\": 599\n },\n \"message\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Human-readable failure message. Supports template expressions, e.g. \\\"Document classified as {{ steps.classify.output.label }}\\\".\"\n }\n },\n \"required\": [\n \"statusCode\",\n \"message\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"not\": {},\n \"description\": \"control.fail never produces output — it terminates the workflow when triggered.\"\n}\n```\n<!-- GENERATED:STEP_CATALOG END -->\n"
|
|
422
|
+
"content": "# Step types\n\nThe catalog is generated from `STEP_SCHEMAS` in\n`@eigenpal/types/src/workflow/step-configs.ts`. **Do not memorize fields\n— introspect.** The CLI reads the live schema:\n\n```bash\n# List every step type with a one-line description\neigenpal workflow step-type list\n\n# Filter by keyword\neigenpal workflow step-type list --search extract\n\n# Full JSON Schema for one type's `config` + `output` + behavioral docs\neigenpal workflow step-type get ai.extract\n```\n\nThe output of `step-type get` is the **authoritative source** for which\nfields each step takes, what shapes they accept, and what behavior they\nimplement at runtime. Server-side validation errors point you here.\n\n## Categories (high-level map)\n\n- `ai.*` — model-backed processing (parse, extract, classify). Cost + latency depend on the model.\n- `transform.*` — deterministic data transforms (set, remove, combine, split, merge,\n script, template, pdf-embed, xlsx-to-json, json-to-xlsx). WASM sandboxed where applicable.\n- `action.*` — external side effects (HTTP, invoke another workflow, website reader).\n- `control.*` — flow control (if, foreach, parallel, parallel_map, wait, human_review, fail).\n\nThe full per-type catalog with field tables is auto-generated below from\n`STEP_SCHEMAS`. The high-level map above tells you which family you want;\nthe catalog tells you what fields it takes.\n\n## When to reach for what\n\n| Use case | Step |\n| --------------------------------- | ------------------------------------------------- |\n| Read a PDF / DOCX / image (native-first) | `ai.parse` with `parseMode: native` or `native-or-ocr` |\n| Read a PDF / DOCX / image (full OCR/vision) | `ai.parse` with `parseMode: ocr` or `vision` |\n| Pull a typed object from text | `ai.extract` with `config.schema` |\n| Pick one label from a fixed set | `ai.classify` with `config.labels` |\n| Reject bad inputs with a 4xx code | `control.fail` (often after `ai.classify`) |\n| Pause for human field review | `control.human_review` (top-level only; see below) |\n| Sum / filter / regex | `transform.script` (NOT Liquid) |\n| Render a DOCX or XLSX template (YAML workflow) | `transform.template` with `tmpl_...` or `./file.xlsx` |\n| Fill a DOCX or XLSX template (runtime agent) | fill-template platform skill |\n| Convert XLSX to JSON | `transform.xlsx-to-json` |\n| Convert JSON rows to XLSX | `transform.json-to-xlsx` |\n| Merge or combine objects | `transform.combine` / `transform.merge` |\n| Conditional execution | `if:` on the step (Liquid) or `control.if` |\n| Map over an input array | `forEach:` on the step or `control.foreach` |\n| Concurrent map over an array | `control.parallel_map` |\n| Independent parallel branches | `control.parallel` |\n| External HTTP call | `action.http` |\n| Call another workflow | `action.invoke-workflow` |\n| Fetch a webpage as markdown | `action.website-reader` |\n\n## Workspace vs Git templates\n\nTwo separate template systems share DOCX/XLSX placeholder syntax but not\nreferences:\n\n| Goal | Mechanism |\n| --- | --- |\n| YAML workflow produces a filled DOCX/XLSX | `transform.template` with `templateId: tmpl_...` or `template: ./templates/foo.xlsx` (CLI push uploads) |\n| Runtime agent produces a filled DOCX/XLSX | fill-template platform skill against Git templates |\n\nGit templates (`agents/<agent>/templates/<slug>/`, shared\n`resources/templates/<slug>/`) never receive a `tmpl_...` ID and cannot be\nused in `transform.template`. Manage Git templates through the agent source\nloop — see the eigenpal skill's **Agent templates (Git source)** section.\n\nWorkspace templates are tenant-scoped `tmpl_...` resources with immutable\n`tmpr_...` revisions. Manage them from the CLI (preferred) or Studio:\n\n```bash\neigenpal workflow templates upload ./templates/roster.xlsx --json\neigenpal workflow templates list --json\neigenpal workflow templates get tmpl_... --json\neigenpal workflow templates smoke ./templates/roster.xlsx --data ./fixture.json --out ./filled.xlsx\neigenpal workflow validate ./workflow.yaml --online\neigenpal workflow push --file workflow.yaml\n```\n\nAuthor YAML with either a live id or a source-controlled path. Push uploads\nthe file (skipped when the SHA-256 still matches) and sends `templateId` +\ncurrent `templateRevisionId` to the server without rewriting the file on disk.\n\n`templateId` must be `tmpl_...` when not using a local path; file IDs\n(`file_...`) are rejected at validate/push.\n\nIn the Office file itself use `{placeholder}` and, for XLSX row expansion,\n`{table:array.prop}`. `{{placeholder}}` is Liquid for YAML `data` mapping\nonly — putting it in an XLSX file fails at fill. Example prototype row:\n\n```yaml\n- name: fill-roster\n type: transform.template\n with:\n templateId: tmpl_...\n data:\n report_title: \"{{ steps.extract.output.title }}\"\n subjects: \"{{ steps.extract.output.subjects }}\"\n```\n\nSpreadsheet cells: `{report_title}`, `{table:subjects.first_name}`,\n`{table:subjects.last_name}`. Null/blank cells stay empty; numbers and\nbooleans keep spreadsheet types; every sheet is filled; existing formulas\nstay formulas.\n\n## Liquid vs `transform.script`\n\n| Logic shape | Use |\n| -------------------------- | -------------------------------- |\n| `steps.x.output.field` | Liquid `{{ ... }}` |\n| `value \\| default: 0` | Liquid |\n| `price \\| times: quantity` | Liquid |\n| array `reduce` / `filter` | `transform.script` |\n| multi-step calc | `transform.script` |\n| conditional branches | `transform.script` or `if:` |\n| date arithmetic | `transform.script` |\n| regex | `transform.script` |\n\n`transform.script` advantages over Liquid:\n\n- Explicit `inputs:` declaration; the validator knows the data deps\n- Typed return annotation drives downstream autocomplete + runtime validation\n- Real JS/TS (`Array.prototype.reduce`, `JSON.parse`, etc.)\n- Sandbox limits prevent infinite loops from blowing up an execution\n\n> The function is **TypeScript**. Each `inputs:` key becomes a parameter\n> in declaration order: `inputs: { items, taxRate }` ⇒\n> `function script(items: ..., taxRate: ...): R { ... }`. The return-type\n> annotation `R` is **required** and IS this step's output schema; there\n> is no separate `outputSchema:` field. Describe what your function\n> actually returns: downstream steps reference its fields via\n> `{{ steps.<this>.output.<field> }}`, so a too-loose annotation (e.g.\n> `unknown`) makes those references unresolvable. The function must\n> `return` (or `throw`) a value.\n\n## Reference / output paths\n\nAfter a step runs, its output is reachable via\n`{{ steps.<name>.output.<field> }}`. The exact field set per type comes\nfrom `step-type get`'s `outputSchema`. Example for `ai.extract`:\n\n```bash\n$ eigenpal workflow step-type get ai.extract | jq '.outputSchema'\n{\n \"type\": \"object\",\n \"properties\": {\n \"extracted\": { /* matches the user-supplied config.schema */ },\n \"rawResponse\": { \"type\": \"string\" },\n \"tokensUsed\": { \"type\": \"number\" }\n }\n}\n```\n\nReference user-supplied schema fields the same way:\n`{{ steps.extract.output.extracted.invoiceNumber }}`.\n\n## Human review — top-level gate only\n\n`control.human_review` pauses the run until a reviewer confirms or corrects\nselected scalar fields, then resumes with the reviewed object or array. Place\nit as a **top-level sequential step** only — nested placement inside\n`control.if`, `control.foreach`, `control.parallel`, `control.parallel_map`, or\n`control.switch` is rejected at publish time.\n\nConfig lives in `step.with` (unlike most other `control.*` steps). Wire data\nfrom an upstream extract or transform, choose a selection mode, and attach run\nfiles for context:\n\n```yaml\n- name: review-invoice\n type: control.human_review\n with:\n data: '{{ steps.extract.output }}'\n metadataFrom: ai.extract\n instructions: Confirm vendor and total before posting.\n selection:\n mode: confidence\n threshold: medium\n missingConfidence: review\n fields:\n /vendor:\n review: always\n /total:\n threshold: high\n fieldMetadata:\n /vendor:\n label: Vendor name\n /total:\n label: Invoice total\n files:\n includeRunInputs: true\n attachments:\n - '{{ steps.render.output.files[0] }}'\n```\n\nWhen `metadataFrom: ai.extract` is set and `data` is the whole extract output\n(`{{ steps.<name>.output }}`), the step strips the reserved `_grounding` key,\nderives categorical confidence (`low`, `medium`, `high`) per field, and reuses\nthe extract schema when `schema` is omitted.\n\n**Selection:** three modes — `confidence` (review fields below threshold),\n`explicit` (review matched include/exclude paths), and `all` (every scalar\nleaf). Thresholds accept numeric `0`–`1` or categorical `low|medium|high`;\nequality auto-approves. Per-path overrides live under `selection.fields` with\n`review: always|never|skip` and optional per-field `threshold`. Prefer\n`selection.fields` over the legacy `fieldMetadata.review`.\n\n**Output:** the complete reviewed data in the same shape as `data`. Reference\ndownstream fields directly: `{{ steps.review-invoice.output.vendor }}`.\n\n**Evaluations:** evaluation runs always simulate review and never pause the\nrun. Use `meta.json` `review` fixtures to assert routing, reasons, and\nsimulated reviewer edits — see `reference/dataset-format.md`. Empty selection\ncontinues immediately without creating a task.\n\n**Limits:** 2 MiB review JSON, 10k scalar leaves, 1 MiB metadata, 2k required\nfields, 100 attachments.\n\nUse `eigenpal workflow step-type get control.human_review` for the full schema.\n\n## Control containers — nested step shape and scoping\n\n`control.parallel`, `control.parallel_map`, `control.foreach`, `control.if`,\nand inline `action.invoke-workflow` (`execution: inline`) contain nested steps. The auto-generated catalog below\ncannot render that shape, so the YAML form lives here. Same for the scoping\nrules — important because the runtime treats nested steps differently from\ntop-level ones, and getting the access path wrong silently returns\n`undefined` (no error).\n\n### `control.parallel` — independent branches\n\nRuns each branch concurrently. Use when several pieces of work share input\nbut do not depend on each other.\n\n```yaml\n- name: enrich\n type: control.parallel\n branches:\n - name: legal\n steps:\n - name: extract-clauses\n type: ai.extract\n with:\n input: '{{ steps.parse.output.markdown }}'\n schema: { clauses: { type: array, items: { type: string } } }\n - name: financial\n steps:\n - name: extract-totals\n type: ai.extract\n with:\n input: '{{ steps.parse.output.markdown }}'\n schema: { total: { type: number } }\n```\n\n**Access the output:** branch step outputs are stored under the parent\nparallel step keyed by branch name, then by inner step name. There is no\nflat top-level entry for branch-internal steps.\n\n```liquid\n{{ steps.enrich.output.legal.extract-clauses.clauses }}\n{{ steps.enrich.output.financial.extract-totals.total }}\n```\n\n**Scope inside a branch:** top-level steps that ran before the parallel,\nplus earlier siblings in the same branch. **Other branches' steps are\nNOT visible** — each branch runs in an isolated child scope.\n\n```liquid\n# Inside branch `legal`, second step:\n{{ steps.parse.output.markdown }} # ancestor: visible\n{{ steps.extract-clauses.output.clauses }} # sibling: visible\n{{ steps.extract-totals.output.total }} # other branch: UNDEFINED\n```\n\n### `control.parallel_map` — fan-out over an array\n\nRuns the inner step block concurrently for each item, up to `concurrency`.\n\n```yaml\n- name: process-pages\n type: control.parallel_map\n items: '{{ steps.split.output.pages }}'\n as: page\n indexAs: i # optional\n concurrency: 5 # default 5, max 50\n steps:\n - name: extract-fields\n type: ai.extract\n with:\n input: '{{ page.text }}'\n schema: { fields: { type: object } }\n```\n\n**Access the output:** iteration results are returned as an array in the\noriginal input order under the parent step's `items` field.\n\n```liquid\n{{ steps.process-pages.output.items[0].extract-fields.fields }}\n{{ steps.process-pages.output.count }} # number of completed iterations\n{{ steps.process-pages.output.totalIterations }}\n```\n\n`steps.extract-fields.output.fields` (the flat form) is **not** addressable\nfrom outside the parallel_map.\n\n**Multi-step iterations only key the LAST step's output into `items[i]`.**\nIf your iteration body has steps `[parse, extract]`, each `items[i]` is\n`{ extract: <extract output> }` — `parse`'s output is gone. To preserve\nintermediate fields, end the body with a `transform.script` that returns\nthe combined shape:\n\n```yaml\nsteps:\n - name: parse\n type: ai.parse\n with: { ... }\n - name: extract\n type: ai.extract\n with: { input: '{{ steps.parse.output.markdown }}', ... }\n - name: combine\n type: transform.script\n with:\n inputs:\n parsed: '{{ steps.parse.output }}'\n extracted: '{{ steps.extract.output }}'\n function: |\n function script(parsed, extracted): { parsed: any; extracted: any } {\n return { parsed, extracted };\n }\n```\n\nNow `items[i].combine.parsed` / `items[i].combine.extracted` both survive.\n\n### `control.foreach` — sequential loop over an array\n\nSame shape as `parallel_map` minus `concurrency`; iterations run in order.\nUse when each iteration depends on the previous, or when concurrency would\ncause downstream rate limits.\n\n```yaml\n- name: per-row\n type: control.foreach\n items: '{{ steps.fetch.output.rows }}'\n as: row\n steps:\n - name: persist\n type: action.http-request\n with:\n method: POST\n url: 'https://example.com/api/rows/{{ row.id }}'\n body: '{{ row }}'\n```\n\n**Access the output:** same shape as parallel_map — an `items[]` array of\nper-iteration results, in iteration order. Same \"only the LAST inner step\nis keyed into `items[i]`\" rule applies (use a trailing `transform.script`\nto preserve intermediate outputs).\n\n> ⚠️ Foreach does NOT isolate scopes — the last iteration's inner step\n> outputs technically persist on the top-level `steps` map and `steps.<inner>.output.x`\n> resolves at runtime. That is an only-the-last-row hazard; the autocomplete\n> hides these names and you should treat `steps.<foreach>.output.items[]`\n> as the only correct access.\n\n### `control.if` — conditional branch\n\n```yaml\n- name: route\n type: control.if\n condition: '{{ input.priority == \"high\" }}'\n then:\n - name: rush-extract\n type: ai.extract\n with: { ... }\n else:\n - name: normal-extract\n type: ai.extract\n with: { ... }\n```\n\n**Access the output:** the `if` step's output is\n\n```ts\n{\n condition: boolean, // evaluated condition\n branch: 'then' | 'else', // which branch ran\n result: <last inner step's output> | null // null when chosen branch is empty\n}\n```\n\nThe canonical way to consume the result is through the if step itself:\n\n```liquid\n{{ steps.route.output.branch }} # \"then\" or \"else\"\n{{ steps.route.output.result.foo }} # field from the branch's last step\n```\n\n`steps.<innerName>.output` references from outside the `if` also resolve\nat runtime (unlike `parallel`/`parallel_map`, `if` does not isolate\nscopes), but the template breaks the moment the OTHER branch runs. Use\n`steps.<if>.output.result` so the access is branch-agnostic — or compute\nthe final shape in a trailing `transform.script` that handles both cases.\n\n### `action.invoke-workflow` (inline) — call another workflow in the same run\n\n```yaml\n- name: invoice-flow\n type: action.invoke-workflow\n with:\n workflow: parse-invoice # target workflow name or wf_ id\n execution: inline # default; omit for same-run execution\n input:\n contract: '{{ input.contract }}'\n```\n\n**Access the output:** the target's declared output fields are exposed as\ntop-level keys under `steps.<this-step>.output`, with a `files` array alongside.\nInvoked workflow steps run in an isolated scope inside the parent run.\n\nFor a separate child run (lineage, fire-and-forget, polling), set `execution: child`.\n\n## Classify-and-fail pattern\n\n`ai.classify` + `control.fail` together let a workflow reject bad inputs\nfast with a typed HTTP-style status code that callers (and evals) can\nmatch against. The classifier picks one label from a closed set; the\nfail step terminates the run when the label is one you do not want to\nprocess.\n\n```yaml\n- name: parse\n type: ai.parse\n with:\n input: '{{ input.document }}'\n\n- name: classify\n type: ai.classify\n with:\n input: '{{ steps.parse.output.text }}'\n labels:\n - name: invoice\n description: A vendor invoice with line items and totals.\n - name: contract\n description: A multi-page contract or agreement.\n - name: other\n description: Anything that is not an invoice or contract.\n\n- name: reject-unsupported\n type: control.fail\n condition: '{{ steps.classify.output.label == \"other\" }}'\n statusCode: 422\n message: 'Unsupported document: {{ steps.classify.output.reason }}'\n```\n\n> `control.fail` config is step-level (no `with:`), matching every other\n> `control.*` step. `condition`, `statusCode`, and `message` sit directly\n> on the step node.\n\nThe synchronous run endpoint surfaces the `statusCode` as the HTTP\nresponse status; async runs persist `{ code, message, step }` to\n`executions.error` so the eval scorer can match against\n`expected.json` with `$error` (see `reference/dataset-format.md`). When\n`condition` is omitted, `control.fail` always fails when reached, so\ncompose with `control.if` for legacy gating.\n\n<!-- GENERATED:STEP_CATALOG START -->\n## Full catalog\n\n_Generated from `STEP_SCHEMAS` in `@eigenpal/types/src/workflow/step-configs.ts`. Do not hand-edit between the GENERATED fences — run `bun run --cwd packages/cli generate:skill`._\n\n### AI steps — model-backed processing\n\n#### `ai.parse` — Parse Document\n\nExtract text from documents (PDF, DOCX, images) using native extraction, OCR, or vision models\n\n**Behavior and examples:** `eigenpal docs read steps/ai/parse`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | Storage reference or template expression for the document |\n| `parseMode` | `\"ocr\"` \\| `\"vision\"` \\| `\"native\"` \\| `\"native-or-ocr\"` | no | | Base parser for PDF/image inputs. OCR is the default; vision uses the selected LLM. `native` extracts PDF embedded text only and never calls OCR or vision. `native-or-ocr` extracts native text, runs page-quality diagnostics, and requests OCR when pages have detectable anomalies (empty, U+FFFD, lone surrogates, forbidden controls, unassigned/noncharacter, heavy PUA). Page selection, subset egress, and billing are provider-dependent. Valid-looking wrong text and literal \"?\" are not flagged. Text and Office files always use local parsing. |\n| `ocrModel` | string | no | | OCR provider ID for PDF/image parsing |\n| `llmModel` | string | no | | LLM provider ID for vision-based parsing |\n| `llmReasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `figureModel` | string | no | | Vision model used only for the optional figure-description pass |\n| `figureReasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `maxConcurrency` | number | no | `3` | Max concurrent VLM batch requests |\n| `pagesPerBatch` | number | no | `5` | Number of page images per VLM request |\n| `pdfRenderScale` | number | no | `1` | Scale factor for rendering PDF pages before VLM parsing. Higher values produce sharper images at larger payload sizes. |\n| `imageQuality` | integer | no | `85` | JPEG quality for rendered PDF page images sent to VLM parsing. Higher values reduce compression artifacts at larger payload sizes. |\n| `prompt` | string | no | | Custom extraction prompt |\n| `languages` | array<string> | no | | OCR language hints |\n| `outputFormat` | `\"plain\"` \\| `\"markdown\"` \\| `\"djot\"` \\| `\"html\"` \\| `\"layout\"` | no | `\"markdown\"` | Format for extracted text. `markdown` (default) keeps structure; `plain` is unstyled text; `djot`/`html` preserve more markup. `layout` is native-PDF spatial text with column gaps preserved as spaces. It requires `parseMode: native` or `parseMode: native-or-ocr` and a PDF — omitted parseMode and `nativeText: true` are not enough. Native mode is layout-only and fail-closed. `native-or-ocr` keeps layout text on accepted native pages and uses OCR markdown on suspect or empty pages, so the document can mix fixed-width layout and markdown. Extractor failure never falls back to another parser. Office, plaintext, and images fail. OCR/vision cannot request `layout`. |\n| `nativeText` | boolean | no | `false` | Extract native/embedded text from PDFs without OCR/VLM. Faster and uses no credits. Falls back to OCR/VLM if the PDF has no embedded text. |\n| `describeFigures` | boolean | no | | Opt-in (default off). After text extraction, detect which pages contain figures with an in-worker layout model, then caption those pages with a vision model and append `<figure>description</figure>` to their text — so image-only pages (property photos, signatures, charts) become findable by text-based steps like ai.split. Note: the layout scan runs over all pages, and the caption step and its vision calls are billed. Skipped for plaintext. |\n| `figureInstructions` | string | no | | Custom instruction for the figure-description pass, e.g. \"Describe each figure; label a handwritten signature as `<figure>signature</figure>` and a stamp as `<figure>stamp</figure>`; for property photos note the room or exterior shown.\" Applied only when describeFigures runs. |\n| `cache` | boolean | no | `false` | When true, reuse a prior parse for identical file bytes and parse settings. Skips OCR/vision on cache hit. Stored in tenant blob storage with no automatic expiry (TTL/lifecycle is a follow-up). Default off. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `document` | object | yes | | |\n| `usage` | object | yes | | |\n| `pages` | array<object> | yes | | |\n| `text` | string | yes | | Combined text from all pages |\n| `parserType` | `\"plaintext\"` \\| `\"office\"` \\| `\"llm-vision\"` \\| `\"ocr\"` | yes | | |\n| `parserVersion` | string | no | | |\n| `model` | string | no | | Model used (for LLM/OCR parsers) |\n| `processingStrategy` | `\"native\"` \\| `\"ocr\"` \\| `\"vision\"` \\| `\"hybrid\"` | no | | How this result was produced: `native` (local PDF text only), `ocr`, `vision`, or `hybrid` (native pages merged with OCR on fallback pages). |\n| `structured` | record<string, unknown> | no | | Canonical structured document with ordered blocks, regions, bounding boxes, tables, figures, and chunks when supported by the parser |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"Storage reference or template expression for the document\"\n },\n \"parseMode\": {\n \"description\": \"Base parser for PDF/image inputs. OCR is the default; vision uses the selected LLM. `native` extracts PDF embedded text only and never calls OCR or vision. `native-or-ocr` extracts native text, runs page-quality diagnostics, and requests OCR when pages have detectable anomalies (empty, U+FFFD, lone surrogates, forbidden controls, unassigned/noncharacter, heavy PUA). Page selection, subset egress, and billing are provider-dependent. Valid-looking wrong text and literal \\\"?\\\" are not flagged. Text and Office files always use local parsing.\",\n \"type\": \"string\",\n \"enum\": [\n \"ocr\",\n \"vision\",\n \"native\",\n \"native-or-ocr\"\n ]\n },\n \"ocrModel\": {\n \"description\": \"OCR provider ID for PDF/image parsing\",\n \"type\": \"string\"\n },\n \"llmModel\": {\n \"description\": \"LLM provider ID for vision-based parsing\",\n \"type\": \"string\"\n },\n \"llmReasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"figureModel\": {\n \"description\": \"Vision model used only for the optional figure-description pass\",\n \"type\": \"string\"\n },\n \"figureReasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"maxConcurrency\": {\n \"description\": \"Max concurrent VLM batch requests\",\n \"default\": 3,\n \"type\": \"number\",\n \"minimum\": 1,\n \"maximum\": 10\n },\n \"pagesPerBatch\": {\n \"description\": \"Number of page images per VLM request\",\n \"default\": 5,\n \"type\": \"number\",\n \"minimum\": 1,\n \"maximum\": 20\n },\n \"pdfRenderScale\": {\n \"description\": \"Scale factor for rendering PDF pages before VLM parsing. Higher values produce sharper images at larger payload sizes.\",\n \"default\": 1,\n \"type\": \"number\",\n \"minimum\": 1,\n \"maximum\": 4\n },\n \"imageQuality\": {\n \"description\": \"JPEG quality for rendered PDF page images sent to VLM parsing. Higher values reduce compression artifacts at larger payload sizes.\",\n \"default\": 85,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 100\n },\n \"prompt\": {\n \"description\": \"Custom extraction prompt\",\n \"type\": \"string\"\n },\n \"languages\": {\n \"description\": \"OCR language hints\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"outputFormat\": {\n \"description\": \"Format for extracted text. `markdown` (default) keeps structure; `plain` is unstyled text; `djot`/`html` preserve more markup. `layout` is native-PDF spatial text with column gaps preserved as spaces. It requires `parseMode: native` or `parseMode: native-or-ocr` and a PDF — omitted parseMode and `nativeText: true` are not enough. Native mode is layout-only and fail-closed. `native-or-ocr` keeps layout text on accepted native pages and uses OCR markdown on suspect or empty pages, so the document can mix fixed-width layout and markdown. Extractor failure never falls back to another parser. Office, plaintext, and images fail. OCR/vision cannot request `layout`.\",\n \"default\": \"markdown\",\n \"type\": \"string\",\n \"enum\": [\n \"plain\",\n \"markdown\",\n \"djot\",\n \"html\",\n \"layout\"\n ]\n },\n \"nativeText\": {\n \"description\": \"Extract native/embedded text from PDFs without OCR/VLM. Faster and uses no credits. Falls back to OCR/VLM if the PDF has no embedded text.\",\n \"default\": false,\n \"type\": \"boolean\"\n },\n \"describeFigures\": {\n \"description\": \"Opt-in (default off). After text extraction, detect which pages contain figures with an in-worker layout model, then caption those pages with a vision model and append `<figure>description</figure>` to their text — so image-only pages (property photos, signatures, charts) become findable by text-based steps like ai.split. Note: the layout scan runs over all pages, and the caption step and its vision calls are billed. Skipped for plaintext.\",\n \"type\": \"boolean\"\n },\n \"figureInstructions\": {\n \"description\": \"Custom instruction for the figure-description pass, e.g. \\\"Describe each figure; label a handwritten signature as `<figure>signature</figure>` and a stamp as `<figure>stamp</figure>`; for property photos note the room or exterior shown.\\\" Applied only when describeFigures runs.\",\n \"type\": \"string\"\n },\n \"cache\": {\n \"description\": \"When true, reuse a prior parse for identical file bytes and parse settings. Skips OCR/vision on cache hit. Stored in tenant blob storage with no automatic expiry (TTL/lifecycle is a follow-up). Default off.\",\n \"default\": false,\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"input\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"document\": {\n \"type\": \"object\",\n \"properties\": {\n \"filename\": {\n \"type\": \"string\"\n },\n \"mimeType\": {\n \"type\": \"string\"\n },\n \"size\": {\n \"type\": \"number\",\n \"description\": \"File size in bytes\"\n },\n \"storageRef\": {\n \"description\": \"Storage reference for the original file\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"filename\",\n \"mimeType\",\n \"size\"\n ],\n \"additionalProperties\": false\n },\n \"usage\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageCount\": {\n \"type\": \"number\"\n },\n \"processingTimeMs\": {\n \"type\": \"number\"\n },\n \"ocrPagesProcessed\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"cached\": {\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"pageCount\"\n ],\n \"additionalProperties\": false\n },\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"number\",\n \"description\": \"0-based page index\"\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Extracted page text (markdown/HTML/plain, or spatial layout text)\"\n },\n \"pageName\": {\n \"description\": \"Page/sheet name (e.g., Excel sheet name)\",\n \"type\": \"string\"\n },\n \"layoutElements\": {\n \"description\": \"Semantic layout elements in reading order\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"paragraph\",\n \"title\",\n \"sectionHeading\",\n \"pageHeader\",\n \"pageFooter\",\n \"pageNumber\",\n \"footnote\",\n \"table\",\n \"figure\",\n \"formulaBlock\"\n ]\n },\n \"content\": {\n \"type\": \"string\"\n },\n \"table\": {\n \"type\": \"object\",\n \"properties\": {\n \"cells\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"content\": {\n \"type\": \"string\"\n },\n \"rowIndex\": {\n \"type\": \"number\"\n },\n \"columnIndex\": {\n \"type\": \"number\"\n },\n \"rowSpan\": {\n \"default\": 1,\n \"type\": \"number\"\n },\n \"columnSpan\": {\n \"default\": 1,\n \"type\": \"number\"\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"content\",\n \"rowIndex\",\n \"columnIndex\",\n \"rowSpan\",\n \"columnSpan\"\n ],\n \"additionalProperties\": false\n }\n },\n \"rowCount\": {\n \"type\": \"number\"\n },\n \"columnCount\": {\n \"type\": \"number\"\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"cells\",\n \"rowCount\",\n \"columnCount\"\n ],\n \"additionalProperties\": false\n },\n \"caption\": {\n \"type\": \"string\"\n },\n \"figureId\": {\n \"type\": \"string\"\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"type\",\n \"content\"\n ],\n \"additionalProperties\": false\n }\n },\n \"words\": {\n \"description\": \"Word-level positions\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"text\": {\n \"type\": \"string\"\n },\n \"confidence\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"text\"\n ],\n \"additionalProperties\": false\n }\n },\n \"lines\": {\n \"description\": \"Line-level positions\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"text\": {\n \"type\": \"string\"\n },\n \"confidence\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"text\"\n ],\n \"additionalProperties\": false\n }\n },\n \"tables\": {\n \"description\": \"Extracted tables\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"cells\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"content\": {\n \"type\": \"string\"\n },\n \"rowIndex\": {\n \"type\": \"number\"\n },\n \"columnIndex\": {\n \"type\": \"number\"\n },\n \"rowSpan\": {\n \"default\": 1,\n \"type\": \"number\"\n },\n \"columnSpan\": {\n \"default\": 1,\n \"type\": \"number\"\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"content\",\n \"rowIndex\",\n \"columnIndex\",\n \"rowSpan\",\n \"columnSpan\"\n ],\n \"additionalProperties\": false\n }\n },\n \"rowCount\": {\n \"type\": \"number\"\n },\n \"columnCount\": {\n \"type\": \"number\"\n },\n \"boundingRegion\": {\n \"type\": \"object\",\n \"properties\": {\n \"polygon\": {\n \"minItems\": 3,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"x\": {\n \"type\": \"number\"\n },\n \"y\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"x\",\n \"y\"\n ],\n \"additionalProperties\": false\n }\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"pageIndex\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"polygon\",\n \"unit\",\n \"pageIndex\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"cells\",\n \"rowCount\",\n \"columnCount\"\n ],\n \"additionalProperties\": false\n }\n },\n \"width\": {\n \"description\": \"Page width\",\n \"type\": \"number\"\n },\n \"height\": {\n \"description\": \"Page height\",\n \"type\": \"number\"\n },\n \"unit\": {\n \"description\": \"Unit for width/height and bounding regions\",\n \"type\": \"string\",\n \"enum\": [\n \"pixel\",\n \"inch\",\n \"point\",\n \"normalized\"\n ]\n },\n \"confidence\": {\n \"description\": \"Overall page confidence\",\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"nativeTextQuality\": {\n \"description\": \"Per-page diagnosis of native-extracted text. Reports objectively detectable anomalies (empty, U+FFFD, forbidden controls, unassigned/noncharacter code points, heavy private-use). Does not certify semantic correctness — valid-looking wrong mappings and literal \\\"?\\\" cannot be distinguished from legitimate text.\",\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"enum\": [\n \"clean\",\n \"empty\",\n \"suspect\"\n ]\n },\n \"reasons\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"enum\": [\n \"empty\",\n \"replacement-character\",\n \"lone-surrogate\",\n \"forbidden-control\",\n \"unassigned\",\n \"noncharacter\",\n \"private-use\"\n ]\n }\n },\n \"counts\": {\n \"type\": \"object\",\n \"properties\": {\n \"codePoints\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"nonWhitespace\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"replacement\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"loneSurrogate\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"forbiddenControl\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"unassigned\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"noncharacter\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"privateUse\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"codePoints\",\n \"nonWhitespace\",\n \"replacement\",\n \"loneSurrogate\",\n \"forbiddenControl\",\n \"unassigned\",\n \"noncharacter\",\n \"privateUse\"\n ],\n \"additionalProperties\": false\n },\n \"profile\": {\n \"type\": \"object\",\n \"properties\": {\n \"replacementRatio\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"loneSurrogateRatio\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"forbiddenControlRatio\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"unassignedRatio\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"noncharacterRatio\": {\n \"type\": \"number\",\n \"minimum\": 0\n },\n \"privateUseRatio\": {\n \"type\": \"number\",\n \"minimum\": 0\n }\n },\n \"required\": [\n \"replacementRatio\",\n \"loneSurrogateRatio\",\n \"forbiddenControlRatio\",\n \"unassignedRatio\",\n \"noncharacterRatio\",\n \"privateUseRatio\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"status\",\n \"reasons\",\n \"counts\",\n \"profile\"\n ],\n \"additionalProperties\": false\n },\n \"provenance\": {\n \"description\": \"Which extractor produced the final text/layout on this page\",\n \"type\": \"object\",\n \"properties\": {\n \"source\": {\n \"type\": \"string\",\n \"enum\": [\n \"native\",\n \"ocr\",\n \"vision\"\n ]\n }\n },\n \"required\": [\n \"source\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"pageIndex\",\n \"text\"\n ],\n \"additionalProperties\": false\n }\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Combined text from all pages\"\n },\n \"parserType\": {\n \"type\": \"string\",\n \"enum\": [\n \"plaintext\",\n \"office\",\n \"llm-vision\",\n \"ocr\"\n ]\n },\n \"parserVersion\": {\n \"type\": \"string\"\n },\n \"model\": {\n \"description\": \"Model used (for LLM/OCR parsers)\",\n \"type\": \"string\"\n },\n \"processingStrategy\": {\n \"description\": \"How this result was produced: `native` (local PDF text only), `ocr`, `vision`, or `hybrid` (native pages merged with OCR on fallback pages).\",\n \"type\": \"string\",\n \"enum\": [\n \"native\",\n \"ocr\",\n \"vision\",\n \"hybrid\"\n ]\n },\n \"structured\": {\n \"description\": \"Canonical structured document with ordered blocks, regions, bounding boxes, tables, figures, and chunks when supported by the parser\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"document\",\n \"usage\",\n \"pages\",\n \"text\",\n \"parserType\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `ai.extract` — Extract Data\n\nExtract structured data from text using AI with a JSON schema\n\n**Behavior and examples:** `eigenpal docs read steps/ai/extract`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | Text content or template expression |\n| `schema` | object | yes | | JSON Schema defining the structure to extract |\n| `prompt` | string | no | | Custom prompt template for extraction |\n| `provider` | string | no | | Provider ID (e.g., \"openai-gpt4o\") |\n| `model` | string | no | | Model override |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `maxInputTokens` | integer | no | | Max input tokens. Truncates input text and logs a warning when exceeded. Omit for no limit. |\n| `grounded` | boolean | no | | Grounding is ON by default: each schema field gets a source span + confidence (high=verbatim, medium=fuzzy, low=ungrounded) under a reserved `_grounding` output key, and fields whose value cannot be located in the source are flagged for human review. Values stay the reliable schema-typed ones. The pass runs through the workspace LLM (any provider) and chunks long documents automatically. Tri-state: unset (default) = on, degrading gracefully to deterministic text alignment (`_grounding._degraded: true`) if no grounding model is available; `true` = strict, the step fails when the grounding model cannot be resolved; `false` = off, no `_grounding` key at all. |\n| `groundingModel` | string | no | | Provider/model for the grounding pass. Defaults to the workspace default LLM. Any configured provider works; the pass only fails the step when `grounded: true` is set explicitly and no model resolves. |\n| `groundingReasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `groundingExamples` | array<object> | no | | Optional few-shot examples pinning grounding to verbatim source text per field. |\n| `reviewOn` | `\"medium_or_low\"` \\| `\"low_only\"` | no | | Which grounding confidences set needsReview on a field. Default: low_only (only fields whose value could not be located in the source). Use medium_or_low to also flag approximate and derived matches. |\n\n**Output:** `record<string, unknown>`\n\n> Extracted structured data matching the provided schema. Unless grounding is disabled (grounded: false), the output also carries a reserved `_grounding` map keyed by dotted field path: `_grounding[\"line_items.0.amount\"] = { confidence: high|medium|low, score?, needsReview, reason?, quote?, citations?, source_span }`, plus reserved `_degraded: true` / `_reason` markers when grounding could not run the shared algorithm or the Eigenpal text pass. Lineage is never included in this JSON.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"Text content or template expression\"\n },\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"object\"\n },\n \"properties\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"required\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"properties\"\n ],\n \"additionalProperties\": false,\n \"description\": \"JSON Schema defining the structure to extract\"\n },\n \"prompt\": {\n \"description\": \"Custom prompt template for extraction\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID (e.g., \\\"openai-gpt4o\\\")\",\n \"type\": \"string\"\n },\n \"model\": {\n \"description\": \"Model override\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"maxInputTokens\": {\n \"description\": \"Max input tokens. Truncates input text and logs a warning when exceeded. Omit for no limit.\",\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"grounded\": {\n \"description\": \"Grounding is ON by default: each schema field gets a source span + confidence (high=verbatim, medium=fuzzy, low=ungrounded) under a reserved `_grounding` output key, and fields whose value cannot be located in the source are flagged for human review. Values stay the reliable schema-typed ones. The pass runs through the workspace LLM (any provider) and chunks long documents automatically. Tri-state: unset (default) = on, degrading gracefully to deterministic text alignment (`_grounding._degraded: true`) if no grounding model is available; `true` = strict, the step fails when the grounding model cannot be resolved; `false` = off, no `_grounding` key at all.\",\n \"type\": \"boolean\"\n },\n \"groundingModel\": {\n \"description\": \"Provider/model for the grounding pass. Defaults to the workspace default LLM. Any configured provider works; the pass only fails the step when `grounded: true` is set explicitly and no model resolves.\",\n \"type\": \"string\"\n },\n \"groundingReasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"groundingExamples\": {\n \"description\": \"Optional few-shot examples pinning grounding to verbatim source text per field.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"text\": {\n \"type\": \"string\"\n },\n \"extractions\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"field\": {\n \"type\": \"string\"\n },\n \"text\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"field\",\n \"text\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"text\",\n \"extractions\"\n ],\n \"additionalProperties\": false\n }\n },\n \"reviewOn\": {\n \"description\": \"Which grounding confidences set needsReview on a field. Default: low_only (only fields whose value could not be located in the source). Use medium_or_low to also flag approximate and derived matches.\",\n \"type\": \"string\",\n \"enum\": [\n \"medium_or_low\",\n \"low_only\"\n ]\n }\n },\n \"required\": [\n \"input\",\n \"schema\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Extracted structured data matching the provided schema. Unless grounding is disabled (grounded: false), the output also carries a reserved `_grounding` map keyed by dotted field path: `_grounding[\\\"line_items.0.amount\\\"] = { confidence: high|medium|low, score?, needsReview, reason?, quote?, citations?, source_span }`, plus reserved `_degraded: true` / `_reason` markers when grounding could not run the shared algorithm or the Eigenpal text pass. Lineage is never included in this JSON.\"\n}\n```\n\n\n#### `ai.split` — Split Document\n\nSplit a parsed document into named sections using an LLM. Consumes ai.parse output; emits per-section page ranges and text ready for downstream ai.extract via control.parallel_map.\n\n**Behavior and examples:** `eigenpal docs read steps/ai/split`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | Template expression resolving to ai.parse output, e.g. \"{{ steps.parse.output }}\" |\n| `sections` | array<object> | yes | | Named sections to find in the document |\n| `rules` | string | no | | Optional natural-language rules appended to the system prompt. E.g. \"End-of-section markers like *Koniec prílohy 2* close the current section.\" |\n| `provider` | string | no | | Provider ID from eigenpal.config.yaml (e.g. \"openai-gpt5.4-mini\"). Falls back to the tenant default LLM provider when omitted. |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `windowTokenBudget` | integer | no | | Override the estimated per-window token ceiling. Oversized pages are subdivided with overlap while retaining their original page index. Defaults to env SPLIT_WINDOW_TOKEN_BUDGET or 20000. Inputs requiring more than 64 LLM windows are rejected before calls begin. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `splits` | array<object> | yes | | Sections found in the document, in page order. Absent sections are omitted. |\n| `sections` | record<string, object> | yes | | The same sections keyed by config name, so a downstream step can reference one directly: `{{ steps.<split>.output.sections.<name>.page_range }}`. Prefer this over filtering `splits`. On a duplicate name the last wins. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"Template expression resolving to ai.parse output, e.g. \\\"{{ steps.parse.output }}\\\"\"\n },\n \"sections\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Stable id, used as the key in output and template references\"\n },\n \"description\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"What this section looks like in the document. Use the document's own terminology (e.g. \\\"Príloha 2 / Anlage 2\\\") and visual cues. Multilingual variants belong here.\"\n },\n \"required\": {\n \"description\": \"If true, log a warn when this section is not found, and lean the LLM toward expecting it. Default: false (sections may be absent).\",\n \"type\": \"boolean\"\n },\n \"endHints\": {\n \"description\": \"Natural-language hints fed to the LLM about what marks the END of this section (e.g. [\\\"PRÍPAD PORUŠENIA ZMLUVY\\\", \\\"*Koniec prílohy*\\\"]). The LLM uses these as guidance — not a regex match — so casing variants, missing diacritics, and multilingual phrasings all close the section correctly.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"name\",\n \"description\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Named sections to find in the document\"\n },\n \"rules\": {\n \"description\": \"Optional natural-language rules appended to the system prompt. E.g. \\\"End-of-section markers like *Koniec prílohy 2* close the current section.\\\"\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID from eigenpal.config.yaml (e.g. \\\"openai-gpt5.4-mini\\\"). Falls back to the tenant default LLM provider when omitted.\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"windowTokenBudget\": {\n \"description\": \"Override the estimated per-window token ceiling. Oversized pages are subdivided with overlap while retaining their original page index. Defaults to env SPLIT_WINDOW_TOKEN_BUDGET or 20000. Inputs requiring more than 64 LLM windows are rejected before calls begin.\",\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"input\",\n \"sections\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"splits\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Section name from the config\"\n },\n \"page_range\": {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n ],\n \"description\": \"[startIndex, endIndex] inclusive, 0-based page indices\"\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ],\n \"description\": \"LLM confidence at the anchor page. Coarse enum (low | medium | high) — numeric scores cluster meaninglessly at 0.85-0.95.\"\n },\n \"notes\": {\n \"type\": \"string\",\n \"description\": \"LLM's justification for the anchor — useful for debugging\"\n },\n \"evidence\": {\n \"description\": \"Structured start-anchor evidence — parse this instead of regexing over `notes`.\",\n \"type\": \"object\",\n \"properties\": {\n \"start_heading_text\": {\n \"description\": \"Verbatim heading text the LLM cited as the section start\",\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"start_page\": {\n \"description\": \"Page where start_heading_text appears\",\n \"anyOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"end_evidence\": {\n \"description\": \"Set when the LLM detected an end-of-section cue (matched against the section's `endHints` or an explicit closing marker). Absent when the section was closed by continuity-fill alone.\",\n \"type\": \"object\",\n \"properties\": {\n \"end_page\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"LAST page of the section (inclusive)\"\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n \"notes\": {\n \"type\": \"string\",\n \"description\": \"LLM's justification for the end. Pair with start `notes` for reconciliation.\"\n }\n },\n \"required\": [\n \"end_page\",\n \"confidence\",\n \"notes\"\n ],\n \"additionalProperties\": false\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Joined per-page content for this section, ready for downstream ai.extract\"\n },\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"pageName\": {\n \"type\": \"string\"\n },\n \"source\": {\n \"type\": \"string\",\n \"enum\": [\n \"anchored\",\n \"inferred\"\n ],\n \"description\": \"Whether this page is direct LLM evidence (anchored) or continuity-filled (inferred)\"\n }\n },\n \"required\": [\n \"pageIndex\",\n \"text\",\n \"source\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Raw per-page records covered by this split (in page order). Iterate when downstream needs per-page context (e.g. control.parallel_map over section pages).\"\n },\n \"pages_anchored\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Pages with direct LLM evidence\"\n },\n \"pages_inferred\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Pages assigned by deterministic continuity fill\"\n }\n },\n \"required\": [\n \"name\",\n \"page_range\",\n \"confidence\",\n \"notes\",\n \"text\",\n \"pages\",\n \"pages_anchored\",\n \"pages_inferred\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Sections found in the document, in page order. Absent sections are omitted.\"\n },\n \"sections\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Section name from the config\"\n },\n \"page_range\": {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n ],\n \"description\": \"[startIndex, endIndex] inclusive, 0-based page indices\"\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ],\n \"description\": \"LLM confidence at the anchor page. Coarse enum (low | medium | high) — numeric scores cluster meaninglessly at 0.85-0.95.\"\n },\n \"notes\": {\n \"type\": \"string\",\n \"description\": \"LLM's justification for the anchor — useful for debugging\"\n },\n \"evidence\": {\n \"description\": \"Structured start-anchor evidence — parse this instead of regexing over `notes`.\",\n \"type\": \"object\",\n \"properties\": {\n \"start_heading_text\": {\n \"description\": \"Verbatim heading text the LLM cited as the section start\",\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"start_page\": {\n \"description\": \"Page where start_heading_text appears\",\n \"anyOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"end_evidence\": {\n \"description\": \"Set when the LLM detected an end-of-section cue (matched against the section's `endHints` or an explicit closing marker). Absent when the section was closed by continuity-fill alone.\",\n \"type\": \"object\",\n \"properties\": {\n \"end_page\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"LAST page of the section (inclusive)\"\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n \"notes\": {\n \"type\": \"string\",\n \"description\": \"LLM's justification for the end. Pair with start `notes` for reconciliation.\"\n }\n },\n \"required\": [\n \"end_page\",\n \"confidence\",\n \"notes\"\n ],\n \"additionalProperties\": false\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Joined per-page content for this section, ready for downstream ai.extract\"\n },\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"pageName\": {\n \"type\": \"string\"\n },\n \"source\": {\n \"type\": \"string\",\n \"enum\": [\n \"anchored\",\n \"inferred\"\n ],\n \"description\": \"Whether this page is direct LLM evidence (anchored) or continuity-filled (inferred)\"\n }\n },\n \"required\": [\n \"pageIndex\",\n \"text\",\n \"source\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Raw per-page records covered by this split (in page order). Iterate when downstream needs per-page context (e.g. control.parallel_map over section pages).\"\n },\n \"pages_anchored\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Pages with direct LLM evidence\"\n },\n \"pages_inferred\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Pages assigned by deterministic continuity fill\"\n }\n },\n \"required\": [\n \"name\",\n \"page_range\",\n \"confidence\",\n \"notes\",\n \"text\",\n \"pages\",\n \"pages_anchored\",\n \"pages_inferred\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"The same sections keyed by config name, so a downstream step can reference one directly: `{{ steps.<split>.output.sections.<name>.page_range }}`. Prefer this over filtering `splits`. On a duplicate name the last wins.\"\n }\n },\n \"required\": [\n \"splits\",\n \"sections\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `ai.segment` — Separate Documents\n\nSeparate a concatenated batch (one big scan) into typed document instances using an LLM. Consumes ai.parse output and a type taxonomy; discovers an unknown number of documents in any order and emits per-document page ranges + text + type, ready for type-specific ai.extract via control.parallel_map. The inverse of ai.split.\n\n**Behavior and examples:** `eigenpal docs read steps/ai/segment`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | Template expression resolving to ai.parse output, e.g. \"{{ steps.parse.output }}\" |\n| `documentTypes` | array<object> | yes | | The document-type taxonomy. The LLM tags each detected document with one of these names, or the reserved \"unknown\" type when none fit. |\n| `rules` | string | no | | Optional natural-language rules appended to the system prompt. |\n| `provider` | string | no | | Provider ID from eigenpal.config.yaml. Falls back to the tenant default LLM provider when omitted. |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `windowTokenBudget` | integer | no | | Override the per-window token ceiling. Defaults to env SPLIT_WINDOW_TOKEN_BUDGET or 20000. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `summary` | object | yes | | Batch-level escalation summary: labelled vs unlabelled counts + docs needing review. |\n| `documents` | array<object> | yes | | Documents discovered in the batch, in page order. The full batch is covered. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"Template expression resolving to ai.parse output, e.g. \\\"{{ steps.parse.output }}\\\"\"\n },\n \"documentTypes\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Type label returned in documents[].type (e.g. \\\"timesheet\\\", \\\"deal-memo\\\")\"\n },\n \"description\": {\n \"description\": \"What this document type looks like — headings, layout, vocabulary, and the cues that mark its FIRST page. Fed to the LLM for both boundary detection and typing.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"The document-type taxonomy. The LLM tags each detected document with one of these names, or the reserved \\\"unknown\\\" type when none fit.\"\n },\n \"rules\": {\n \"description\": \"Optional natural-language rules appended to the system prompt.\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID from eigenpal.config.yaml. Falls back to the tenant default LLM provider when omitted.\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"windowTokenBudget\": {\n \"description\": \"Override the per-window token ceiling. Defaults to env SPLIT_WINDOW_TOKEN_BUDGET or 20000.\",\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"input\",\n \"documentTypes\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"summary\": {\n \"type\": \"object\",\n \"properties\": {\n \"total\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"Total documents discovered.\"\n },\n \"labelled\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"Documents assigned a known taxonomy type.\"\n },\n \"unlabelled\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"Documents the segmenter could not label (type \\\"unknown\\\") — escalate to a human.\"\n },\n \"lowConfidence\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"Documents with low boundary/type confidence.\"\n },\n \"byType\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Per-type document counts.\"\n },\n \"needsReview\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"index\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"type\": {\n \"type\": \"string\"\n },\n \"page_range\": {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n ]\n },\n \"reason\": {\n \"type\": \"string\",\n \"enum\": [\n \"unlabelled\",\n \"low_confidence\"\n ]\n },\n \"notes\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"index\",\n \"type\",\n \"page_range\",\n \"reason\",\n \"notes\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Documents to escalate to a human operator, with the reason for each.\"\n }\n },\n \"required\": [\n \"total\",\n \"labelled\",\n \"unlabelled\",\n \"lowConfidence\",\n \"byType\",\n \"needsReview\"\n ],\n \"additionalProperties\": false,\n \"description\": \"Batch-level escalation summary: labelled vs unlabelled counts + docs needing review.\"\n },\n \"documents\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"index\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991,\n \"description\": \"0-based position of this document in the batch\"\n },\n \"type\": {\n \"type\": \"string\",\n \"description\": \"Chosen type name from the taxonomy, or \\\"unknown\\\"\"\n },\n \"page_range\": {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n ],\n \"description\": \"[startIndex, endIndex] inclusive, 0-based page indices\"\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ],\n \"description\": \"LLM confidence at the boundary + type. Coarse enum (low | medium | high).\"\n },\n \"notes\": {\n \"type\": \"string\",\n \"description\": \"LLM justification for the boundary — useful for debugging\"\n },\n \"evidence\": {\n \"description\": \"Structured start evidence — parse this instead of regexing over `notes`.\",\n \"type\": \"object\",\n \"properties\": {\n \"start_heading_text\": {\n \"description\": \"Verbatim heading/title text the LLM cited as the document start\",\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"start_page\": {\n \"description\": \"Page where the start appears\",\n \"anyOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"additionalProperties\": false\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Joined per-page content for this document, ready for downstream ai.extract\"\n },\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"text\": {\n \"type\": \"string\"\n },\n \"pageName\": {\n \"type\": \"string\"\n },\n \"source\": {\n \"type\": \"string\",\n \"enum\": [\n \"anchored\",\n \"inferred\"\n ]\n }\n },\n \"required\": [\n \"pageIndex\",\n \"text\",\n \"source\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Raw per-page records covered by this document, in page order.\"\n },\n \"pages_anchored\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Pages with direct LLM boundary evidence\"\n },\n \"pages_inferred\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Pages assigned by deterministic continuity fill\"\n }\n },\n \"required\": [\n \"index\",\n \"type\",\n \"page_range\",\n \"confidence\",\n \"notes\",\n \"text\",\n \"pages\",\n \"pages_anchored\",\n \"pages_inferred\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Documents discovered in the batch, in page order. The full batch is covered.\"\n }\n },\n \"required\": [\n \"summary\",\n \"documents\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `ai.classify` — Classify\n\nClassify a document or text into one of a fixed label set using an LLM. Output exposes the picked label (constrained to the configured names), a coarse confidence, and a short justification. Pair with control.fail to reject documents that match an undesired label.\n\n**Behavior and examples:** `eigenpal docs read steps/ai/classify`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | Template expression for the text to classify. Typically the output of ai.parse, e.g. \"{{ steps.parse.output.text }}\". |\n| `labels` | array<object> | yes | | Allowed labels. The LLM is constrained to pick exactly one of these names. |\n| `prompt` | string | no | | Custom classification instructions appended to the system prompt. Use to clarify edge cases or emphasize evidence the model should weigh. |\n| `provider` | string | no | | Provider ID from eigenpal.config.yaml (e.g. \"openai-gpt4o-mini\"). Falls back to the tenant default LLM provider when omitted. |\n| `model` | string | no | | Model override (advanced) |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `maxInputTokens` | integer | no | | Max input tokens. Truncates input text when exceeded. Omit for no limit. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `label` | string | yes | | The selected label name (one of the configured labels). Compare against literal strings to gate downstream steps. |\n| `confidence` | `\"low\"` \\| `\"medium\"` \\| `\"high\"` | yes | | LLM confidence in the classification. Coarse enum — numeric scores cluster meaninglessly at 0.85-0.95. |\n| `reason` | string | yes | | Short justification for the chosen label — useful for debugging. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"Template expression for the text to classify. Typically the output of ai.parse, e.g. \\\"{{ steps.parse.output.text }}\\\".\"\n },\n \"labels\": {\n \"minItems\": 2,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"The label value returned in output.label. Keep it short and stable (used in downstream conditions).\"\n },\n \"description\": {\n \"description\": \"What documents fall into this label. Fed to the LLM as guidance.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Allowed labels. The LLM is constrained to pick exactly one of these names.\"\n },\n \"prompt\": {\n \"description\": \"Custom classification instructions appended to the system prompt. Use to clarify edge cases or emphasize evidence the model should weigh.\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID from eigenpal.config.yaml (e.g. \\\"openai-gpt4o-mini\\\"). Falls back to the tenant default LLM provider when omitted.\",\n \"type\": \"string\"\n },\n \"model\": {\n \"description\": \"Model override (advanced)\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"maxInputTokens\": {\n \"description\": \"Max input tokens. Truncates input text when exceeded. Omit for no limit.\",\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"input\",\n \"labels\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"label\": {\n \"type\": \"string\",\n \"description\": \"The selected label name (one of the configured labels). Compare against literal strings to gate downstream steps.\"\n },\n \"confidence\": {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ],\n \"description\": \"LLM confidence in the classification. Coarse enum — numeric scores cluster meaninglessly at 0.85-0.95.\"\n },\n \"reason\": {\n \"type\": \"string\",\n \"description\": \"Short justification for the chosen label — useful for debugging.\"\n }\n },\n \"required\": [\n \"label\",\n \"confidence\",\n \"reason\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `ai.classify-pages` — Label Pages\n\nAssign zero or more labels to each page independently (multi-label) using an LLM. Consumes ai.parse output; emits per-page labels and a byLabel map (label -> page indices) that supports NON-contiguous selections. Feed byLabel.<label> straight into ai.vision `pageIndices` to inspect scattered pages of a type (e.g. every signature or property-photo page).\n\n**Behavior and examples:** `eigenpal docs read steps/ai/classify-pages`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | The ai.parse output to label, e.g. \"{{ steps.parse.output }}\". Each page is labelled independently. |\n| `labels` | array<string \\| object> | yes | | Labels a page can carry. A page may match ZERO, ONE, or MANY of them. Each entry is a bare name (e.g. \"photo\") or { name, description }. Names must be unique. |\n| `prompt` | string | no | | Extra classification guidance appended to the system prompt. |\n| `provider` | string | no | | Provider ID from eigenpal.config.yaml. Falls back to the tenant default. |\n| `model` | string | no | | Model override (advanced). |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `windowTokenBudget` | integer | no | | Per-window token ceiling. Pages are packed into windows under this budget; one LLM call per window. Defaults to env SPLIT_WINDOW_TOKEN_BUDGET (20000). |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `pages` | array<object> | yes | | Every input page, in order, with the labels the model assigned to it. |\n| `byLabel` | record<string, array<integer>> | yes | | Label name -> ascending page indices carrying it. Every configured label is present (empty array when no page matched), so dot access is always safe: `{{ steps.<step>.output.byLabel.<label> }}` resolves to a number[] ready for ai.vision `pageIndices`. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"The ai.parse output to label, e.g. \\\"{{ steps.parse.output }}\\\". Each page is labelled independently.\"\n },\n \"labels\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Label value — the key in output.byLabel and an entry in pages[].labels.\"\n },\n \"description\": {\n \"description\": \"What a page must show to earn this label. Fed to the LLM as guidance.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\"\n ],\n \"additionalProperties\": false\n }\n ]\n },\n \"description\": \"Labels a page can carry. A page may match ZERO, ONE, or MANY of them. Each entry is a bare name (e.g. \\\"photo\\\") or { name, description }. Names must be unique.\"\n },\n \"prompt\": {\n \"description\": \"Extra classification guidance appended to the system prompt.\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID from eigenpal.config.yaml. Falls back to the tenant default.\",\n \"type\": \"string\"\n },\n \"model\": {\n \"description\": \"Model override (advanced).\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"windowTokenBudget\": {\n \"description\": \"Per-window token ceiling. Pages are packed into windows under this budget; one LLM call per window. Defaults to env SPLIT_WINDOW_TOKEN_BUDGET (20000).\",\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"input\",\n \"labels\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"labels\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Labels assigned to this page (possibly empty).\"\n }\n },\n \"required\": [\n \"pageIndex\",\n \"labels\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Every input page, in order, with the labels the model assigned to it.\"\n },\n \"byLabel\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n },\n \"description\": \"Label name -> ascending page indices carrying it. Every configured label is present (empty array when no page matched), so dot access is always safe: `{{ steps.<step>.output.byLabel.<label> }}` resolves to a number[] ready for ai.vision `pageIndices`.\"\n }\n },\n \"required\": [\n \"pages\",\n \"byLabel\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `ai.vision` — Inspect Pages (Vision)\n\nInspect rendered page images with a vision model and return structured JSON matching a schema. The visual counterpart to Extract Data: use it for conclusions that live in the pixels rather than the text (is the document signed? are the photos usable?). Renders PDF, image, or Office/Word inputs; route to specific pages with an ai.split page range to keep it cheap.\n\n**Behavior and examples:** `eigenpal docs read steps/ai/vision`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `document` | string | yes | | Template expression resolving to the input file (PDF, image, or Office document). |\n| `pageFrom` | integer \\| string | no | | First page to inspect (0-based, inclusive). Optional. Accepts a template expression. When omitted, starts from the first page. |\n| `pageTo` | integer \\| string | no | | Last page to inspect (0-based, inclusive). Optional. Accepts a template expression. When omitted, runs to the last page. When BOTH pageFrom and pageTo are omitted, the whole document is inspected in chunks of maxPages (divide-and-conquer, results merged). |\n| `pageIndices` | array<integer> \\| string | no | | Explicit list of 0-based page indices to inspect — supports NON-contiguous selections, e.g. [4, 11, 19], or a template resolving to a number[] such as \"{{ steps.label_pages.output.byLabel.photo }}\". Highest priority: when set, pageFrom/pageTo and pages are ignored. |\n| `schema` | object | yes | | JSON Schema defining the structure the vision model should return. |\n| `prompt` | string | no | | Optional instruction refining the extraction. The schema drives it when omitted. |\n| `provider` | string | no | | Provider ID (must support vision). |\n| `model` | string | no | | Model override (must support vision). |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `renderScale` | number | no | | PDF render scale (1.0 = 72 DPI). Raise for small text or weak VLM OCR. Capped at 6 to avoid oversized page rasters. |\n| `imageQuality` | integer | no | | JPEG quality for rendered pages (default 85). |\n| `maxPages` | integer | no | | Chunk size: the maximum pages sent to the vision model in a single call. When the inspected range (or the whole document) is larger, it is split into chunks of this size and the per-chunk results are merged by a reduce pass. The reduce follows each field's DESCRIPTION: a boolean described as \"present on any page\" is OR-ed, one described as \"true for every page\" is AND-ed, and list fields are concatenated. The merge is most reliable for boolean/scalar claims; for schemas that AGGREGATE long lists across chunks it can still drop or reorder items, so prefer a bounded page range for large list extraction. Default 20, capped at 100. |\n\n**Output:** `record<string, unknown>`\n\n> Structured data matching the provided schema. The output also carries a reserved `_vision` key recording the inspected source document ref and the page indices that were rendered, so the UI can re-rasterize exactly those pages client-side. `_vision` is excluded from user-schema validation.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"document\": {\n \"type\": \"string\",\n \"description\": \"Template expression resolving to the input file (PDF, image, or Office document).\"\n },\n \"pageFrom\": {\n \"description\": \"First page to inspect (0-based, inclusive). Optional. Accepts a template expression. When omitted, starts from the first page.\",\n \"anyOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"pageTo\": {\n \"description\": \"Last page to inspect (0-based, inclusive). Optional. Accepts a template expression. When omitted, runs to the last page. When BOTH pageFrom and pageTo are omitted, the whole document is inspected in chunks of maxPages (divide-and-conquer, results merged).\",\n \"anyOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"pageIndices\": {\n \"description\": \"Explicit list of 0-based page indices to inspect — supports NON-contiguous selections, e.g. [4, 11, 19], or a template resolving to a number[] such as \\\"{{ steps.label_pages.output.byLabel.photo }}\\\". Highest priority: when set, pageFrom/pageTo and pages are ignored.\",\n \"anyOf\": [\n {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"pages\": {\n \"description\": \"DEPRECATED — prefer pageIndices (scattered) or pageFrom/pageTo (contiguous). Which pages to inspect: \\\"all\\\", a single [start,end] range, a list of ranges, or a template expression resolving to one of those.\",\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"all\"\n },\n {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n }\n ],\n \"description\": \"Inclusive 0-based [start, end] page range, matching ai.split page_range.\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n }\n ],\n \"description\": \"Inclusive 0-based [start, end] page range, matching ai.split page_range.\"\n }\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"object\"\n },\n \"properties\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"required\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"properties\"\n ],\n \"additionalProperties\": false,\n \"description\": \"JSON Schema defining the structure the vision model should return.\"\n },\n \"prompt\": {\n \"description\": \"Optional instruction refining the extraction. The schema drives it when omitted.\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID (must support vision).\",\n \"type\": \"string\"\n },\n \"model\": {\n \"description\": \"Model override (must support vision).\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"renderScale\": {\n \"description\": \"PDF render scale (1.0 = 72 DPI). Raise for small text or weak VLM OCR. Capped at 6 to avoid oversized page rasters.\",\n \"type\": \"number\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 6\n },\n \"imageQuality\": {\n \"description\": \"JPEG quality for rendered pages (default 85).\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 100\n },\n \"maxPages\": {\n \"description\": \"Chunk size: the maximum pages sent to the vision model in a single call. When the inspected range (or the whole document) is larger, it is split into chunks of this size and the per-chunk results are merged by a reduce pass. The reduce follows each field's DESCRIPTION: a boolean described as \\\"present on any page\\\" is OR-ed, one described as \\\"true for every page\\\" is AND-ed, and list fields are concatenated. The merge is most reliable for boolean/scalar claims; for schemas that AGGREGATE long lists across chunks it can still drop or reorder items, so prefer a bounded page range for large list extraction. Default 20, capped at 100.\",\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 100\n }\n },\n \"required\": [\n \"document\",\n \"schema\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Structured data matching the provided schema. The output also carries a reserved `_vision` key recording the inspected source document ref and the page indices that were rendered, so the UI can re-rasterize exactly those pages client-side. `_vision` is excluded from user-schema validation.\"\n}\n```\n\n\n#### `ai.search-files` — Search Files\n\nInvestigate a ZIP archive with a question and return an answer plus file citations. Read-only: the step opens files in the archive and never writes back. ZIP is the only accepted format in this version. Optionally scope the search to a folder prefix and cap how many files and investigation turns may run.\n\n**Behavior and examples:** `eigenpal docs read steps/ai/search-files`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `archive` | string | yes | | Template expression or file reference for the ZIP archive to inspect. ZIP is the only accepted format in this version. |\n| `query` | string | yes | | Natural-language question to answer from files inside the archive. |\n| `root` | string | no | | Optional folder prefix inside the archive. When set, only files under this path are in scope. |\n| `provider` | string | no | | Provider ID from eigenpal.config.yaml. Falls back to the tenant default LLM provider when omitted. |\n| `model` | string | no | | Model override (advanced). |\n| `reasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n| `maxIterations` | integer | no | `12` | Maximum investigation turns. Default 12, capped at 50. |\n| `maxFiles` | integer | no | `50` | Maximum files the step may open. Default 50, capped at 200. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `answer` | string | yes | | The investigation answer. Citations point at inspected files; the answer text itself is model-generated. |\n| `citations` | array<object> | yes | | Citations to files the step inspected. Excerpts, when present, are copied from inspected text. |\n| `filesInspected` | array<string> | yes | | Archive-relative paths of files the step opened during investigation. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"archive\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Template expression or file reference for the ZIP archive to inspect. ZIP is the only accepted format in this version.\"\n },\n \"query\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Natural-language question to answer from files inside the archive.\"\n },\n \"root\": {\n \"description\": \"Optional folder prefix inside the archive. When set, only files under this path are in scope.\",\n \"type\": \"string\"\n },\n \"provider\": {\n \"description\": \"Provider ID from eigenpal.config.yaml. Falls back to the tenant default LLM provider when omitted.\",\n \"type\": \"string\"\n },\n \"model\": {\n \"description\": \"Model override (advanced).\",\n \"type\": \"string\"\n },\n \"reasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n },\n \"maxIterations\": {\n \"description\": \"Maximum investigation turns. Default 12, capped at 50.\",\n \"default\": 12,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 50\n },\n \"maxFiles\": {\n \"description\": \"Maximum files the step may open. Default 50, capped at 200.\",\n \"default\": 50,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 200\n }\n },\n \"required\": [\n \"archive\",\n \"query\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"answer\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"The investigation answer. Citations point at inspected files; the answer text itself is model-generated.\"\n },\n \"citations\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"path\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Archive-relative path of a file the step inspected and cited.\"\n },\n \"excerpt\": {\n \"description\": \"Optional excerpt copied from the inspected file text. Omitted when no verbatim excerpt was supplied.\",\n \"type\": \"string\",\n \"maxLength\": 2000\n }\n },\n \"required\": [\n \"path\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Citations to files the step inspected. Excerpts, when present, are copied from inspected text.\"\n },\n \"filesInspected\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Archive-relative paths of files the step opened during investigation.\"\n }\n },\n \"required\": [\n \"answer\",\n \"citations\",\n \"filesInspected\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `ai.figure-crops` — Figure Crops\n\nCrop and caption figure elements from a parsed PDF. Renders the source document, crops each kind=figure bbox from ai.parse output, and describes the crop with a vision model. Output is captions plus normalized bboxes for evidence viewers.\n\n**Behavior and examples:** `eigenpal docs read steps/ai/figure-crops`\n\n**Durable retry:** Provider request retries are separate; the workflow engine does not durably retry this step.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | Storage reference or template expression for the source PDF |\n| `parseOutput` | unknown | yes | | ai.parse output object (template {{steps.<parse>.output}}); figure elements are read from its `elements` array |\n| `model` | string | no | | Vision model for captions (defaults to workspace model) |\n| `figureInstructions` | string | no | | Custom caption instruction; applied per figure crop |\n| `maxFigures` | integer | no | `40` | Cap on figures processed per document |\n| `minAreaFrac` | number | no | `0` | Skip figures below this page-area fraction (logos/decorations) |\n| `includeCrops` | boolean | no | `false` | Include base64 JPEG crops in output (heavy; default off — captions + bboxes only) |\n| `renderScale` | number | no | `1` | Scale factor for rendering PDF pages before cropping |\n| `imageQuality` | integer | no | `85` | JPEG quality for crops |\n| `llmReasoningEffort` | `\"none\"` \\| `\"minimal\"` \\| `\"low\"` \\| `\"medium\"` \\| `\"high\"` \\| `\"xhigh\"` \\| `\"max\"` | no | | Reasoning effort for models that support it. Omit to use the selected model's default. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `figures` | array<object> | yes | | |\n| `_figureCrops` | object | yes | | |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"Storage reference or template expression for the source PDF\"\n },\n \"parseOutput\": {\n \"description\": \"ai.parse output object (template {{steps.<parse>.output}}); figure elements are read from its `elements` array\"\n },\n \"model\": {\n \"description\": \"Vision model for captions (defaults to workspace model)\",\n \"type\": \"string\"\n },\n \"figureInstructions\": {\n \"description\": \"Custom caption instruction; applied per figure crop\",\n \"type\": \"string\"\n },\n \"maxFigures\": {\n \"description\": \"Cap on figures processed per document\",\n \"default\": 40,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 100\n },\n \"minAreaFrac\": {\n \"description\": \"Skip figures below this page-area fraction (logos/decorations)\",\n \"default\": 0,\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n \"includeCrops\": {\n \"description\": \"Include base64 JPEG crops in output (heavy; default off — captions + bboxes only)\",\n \"default\": false,\n \"type\": \"boolean\"\n },\n \"renderScale\": {\n \"description\": \"Scale factor for rendering PDF pages before cropping\",\n \"default\": 1,\n \"type\": \"number\",\n \"minimum\": 1,\n \"maximum\": 4\n },\n \"imageQuality\": {\n \"description\": \"JPEG quality for crops\",\n \"default\": 85,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 100\n },\n \"llmReasoningEffort\": {\n \"description\": \"Reasoning effort for models that support it. Omit to use the selected model's default.\",\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\"\n ]\n }\n },\n \"required\": [\n \"input\",\n \"parseOutput\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"figures\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\"\n },\n \"page\": {\n \"type\": \"number\"\n },\n \"bboxNorm\": {\n \"anyOf\": [\n {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"number\"\n }\n ]\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"areaFrac\": {\n \"type\": \"number\"\n },\n \"caption\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"cropJpegBase64\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"id\",\n \"page\",\n \"bboxNorm\",\n \"areaFrac\",\n \"caption\"\n ],\n \"additionalProperties\": false\n }\n },\n \"_figureCrops\": {\n \"type\": \"object\",\n \"properties\": {\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"number\"\n }\n },\n \"model\": {\n \"type\": \"string\"\n },\n \"skippedSmall\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"pages\",\n \"skippedSmall\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"figures\",\n \"_figureCrops\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n### Transform steps — deterministic data transforms\n\n#### `transform.set` — Set Value\n\nSet key-value pairs in the output object\n\n**Behavior and examples:** `eigenpal docs read steps/transform/set`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `fields` | record<string, unknown> | yes | | Key-value pairs to set in output |\n| `input` | record<string, unknown> | no | | Base object to extend |\n\n**Output:** `record<string, unknown>`\n\n> Object with all fields set\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"fields\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Key-value pairs to set in output\"\n },\n \"input\": {\n \"description\": \"Base object to extend\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"fields\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Object with all fields set\"\n}\n```\n\n\n#### `transform.remove` — Remove Fields\n\nRemove specified fields from an object\n\n**Behavior and examples:** `eigenpal docs read steps/transform/remove`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | record<string, unknown> | no | | Object to remove fields from |\n| `fields` | array<string> | yes | | Field names to remove |\n\n**Output:** `record<string, unknown>`\n\n> Object with fields removed\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"description\": \"Object to remove fields from\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"fields\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Field names to remove\"\n }\n },\n \"required\": [\n \"fields\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Object with fields removed\"\n}\n```\n\n\n#### `transform.combine` — Combine Data\n\nMerge multiple objects or concatenate arrays\n\n**Behavior and examples:** `eigenpal docs read steps/transform/combine`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `sources` | array<string> | yes | | Template expressions for sources to combine |\n| `target` | string | yes | | Target path in output |\n| `mode` | `\"merge\"` \\| `\"concat\"` \\| `\"deep\"` | no | `\"merge\"` | |\n\n**Output:** `record<string, unknown> \\| array<unknown>`\n\n> Combined result\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"sources\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Template expressions for sources to combine\"\n },\n \"target\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Target path in output\"\n },\n \"mode\": {\n \"default\": \"merge\",\n \"type\": \"string\",\n \"enum\": [\n \"merge\",\n \"concat\",\n \"deep\"\n ]\n }\n },\n \"required\": [\n \"sources\",\n \"target\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n {\n \"type\": \"array\",\n \"items\": {}\n }\n ],\n \"description\": \"Combined result\"\n}\n```\n\n\n#### `transform.split` — Split Data\n\nSplit a string by delimiter or extract keys from an object\n\n**Behavior and examples:** `eigenpal docs read steps/transform/split`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string \\| record<string, unknown> | no | | String or object to split |\n| `source` | string | no | | Template expression for source |\n| `by` | string | no | `\",\"` | Delimiter for string splitting |\n| `delimiter` | string | no | | Alias for by |\n| `limit` | number | no | | Max number of splits |\n| `keys` | array<string> | no | | Keys to extract from object |\n\n**Output:** `array<string> \\| object`\n\n> Split result - array or extracted/remaining object\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"description\": \"String or object to split\",\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n ]\n },\n \"source\": {\n \"description\": \"Template expression for source\",\n \"type\": \"string\"\n },\n \"by\": {\n \"description\": \"Delimiter for string splitting\",\n \"default\": \",\",\n \"type\": \"string\"\n },\n \"delimiter\": {\n \"description\": \"Alias for by\",\n \"type\": \"string\"\n },\n \"limit\": {\n \"description\": \"Max number of splits\",\n \"type\": \"number\"\n },\n \"keys\": {\n \"description\": \"Keys to extract from object\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"anyOf\": [\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"extracted\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"remaining\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"extracted\",\n \"remaining\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"description\": \"Split result - array or extracted/remaining object\"\n}\n```\n\n\n#### `transform.merge` — Merge Inputs\n\nMerge multiple named inputs into a single output\n\n**Behavior and examples:** `eigenpal docs read steps/transform/merge`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `preservePortNames` | boolean | no | `true` | |\n| `outputKey` | string | no | | |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `items` | array<unknown> | yes | | |\n| `count` | number | yes | | |\n| `merged` | record<string, unknown> | no | | |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"preservePortNames\": {\n \"default\": true,\n \"type\": \"boolean\"\n },\n \"outputKey\": {\n \"type\": \"string\"\n }\n },\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"items\": {\n \"type\": \"array\",\n \"items\": {}\n },\n \"count\": {\n \"type\": \"number\"\n },\n \"merged\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"items\",\n \"count\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.template` — Fill Template\n\nFill a DOCX or XLSX workspace template with data from previous steps. Reference a tmpl_... id, or a local ./templates/file.xlsx path (CLI push uploads it). Git agent templates use the fill-template skill instead and cannot be referenced here. File IDs (file_...) are not accepted. Placeholders are auto-detected. In the Office file use {placeholder} and {table:array.prop}; {{placeholder}} in an XLSX file is rejected ({{ }} is YAML Liquid only).\n\n**Behavior and examples:** `eigenpal docs read steps/transform/template`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `templateId` | string | no | | Workspace template ID (tmpl_...) from the Templates table (DOCX or XLSX). Git agent templates cannot be referenced here. File IDs (file_...) are not accepted. Mutually exclusive with `template`. |\n| `template` | string | no | | Local DOCX or XLSX path relative to the workflow YAML (e.g. ./templates/foo.xlsx). CLI push keeps the real path inside the workflow project unless you pass --allow-external-templates, uploads unmatched files as new tmpl_ resources, and does not rewrite the source YAML. Mutually exclusive with templateId. |\n| `templateRevisionId` | string | no | | Immutable template revision ID (tmpr_...). Pin this when executions must keep using the same template bytes after replacements. Requires templateId; local `template` paths are pinned automatically on push. |\n| `data` | record<string, unknown> | yes | | Data object to merge into template. Each key must be explicitly defined - cannot pass a whole object as single expression. |\n| `outputFilename` | string | no | | Output filename - supports {{field}} syntax; .docx or .xlsx extension is added if omitted |\n| `highlightNotFound` | boolean | no | `true` | Highlight missing variables with red-colored text in the output document (DOCX only; ignored for XLSX) |\n| `notFoundText` | string | no | `\"NOT FOUND\"` | Text to display for missing variables when highlightNotFound is enabled (DOCX only; ignored for XLSX) |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `fileId` | string | yes | | File ID from files table |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"templateId\": {\n \"description\": \"Workspace template ID (tmpl_...) from the Templates table (DOCX or XLSX). Git agent templates cannot be referenced here. File IDs (file_...) are not accepted. Mutually exclusive with `template`.\",\n \"type\": \"string\",\n \"pattern\": \"^tmpl_[A-Za-z0-9_-]{21}$\"\n },\n \"template\": {\n \"description\": \"Local DOCX or XLSX path relative to the workflow YAML (e.g. ./templates/foo.xlsx). CLI push keeps the real path inside the workflow project unless you pass --allow-external-templates, uploads unmatched files as new tmpl_ resources, and does not rewrite the source YAML. Mutually exclusive with templateId.\",\n \"type\": \"string\"\n },\n \"templateRevisionId\": {\n \"description\": \"Immutable template revision ID (tmpr_...). Pin this when executions must keep using the same template bytes after replacements. Requires templateId; local `template` paths are pinned automatically on push.\",\n \"type\": \"string\",\n \"pattern\": \"^tmpr_[A-Za-z0-9_-]{21}$\"\n },\n \"data\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Data object to merge into template. Each key must be explicitly defined - cannot pass a whole object as single expression.\"\n },\n \"outputFilename\": {\n \"description\": \"Output filename - supports {{field}} syntax; .docx or .xlsx extension is added if omitted\",\n \"type\": \"string\"\n },\n \"highlightNotFound\": {\n \"description\": \"Highlight missing variables with red-colored text in the output document (DOCX only; ignored for XLSX)\",\n \"default\": true,\n \"type\": \"boolean\"\n },\n \"notFoundText\": {\n \"description\": \"Text to display for missing variables when highlightNotFound is enabled (DOCX only; ignored for XLSX)\",\n \"default\": \"NOT FOUND\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"data\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"fileId\": {\n \"type\": \"string\",\n \"description\": \"File ID from files table\"\n }\n },\n \"required\": [\n \"fileId\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.pdf-embed` — Embed PDF Text\n\nEmbed OCR text layer into scanned PDFs/images to make them searchable\n\n**Behavior and examples:** `eigenpal docs read steps/transform/pdf-embed`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | File input - template expression e.g. {{input.document}} |\n| `parseResult` | string | yes | | Parse result - template expression e.g. {{steps.parse.output}} |\n| `outputFilename` | string | no | | Output filename - supports {{filename}} syntax |\n| `confidenceThreshold` | number | no | `0.7` | Minimum OCR confidence (0-1) to include a word |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `fileId` | string | yes | | File ID from files table |\n| `pageCount` | number | yes | | Number of pages in the output PDF |\n| `wordCount` | number | yes | | Number of words embedded |\n| `text` | string | yes | | Extracted text from the document |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"File input - template expression e.g. {{input.document}}\"\n },\n \"parseResult\": {\n \"type\": \"string\",\n \"description\": \"Parse result - template expression e.g. {{steps.parse.output}}\"\n },\n \"outputFilename\": {\n \"description\": \"Output filename - supports {{filename}} syntax\",\n \"type\": \"string\"\n },\n \"confidenceThreshold\": {\n \"description\": \"Minimum OCR confidence (0-1) to include a word\",\n \"default\": 0.7,\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n }\n },\n \"required\": [\n \"input\",\n \"parseResult\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"fileId\": {\n \"type\": \"string\",\n \"description\": \"File ID from files table\"\n },\n \"pageCount\": {\n \"type\": \"number\",\n \"description\": \"Number of pages in the output PDF\"\n },\n \"wordCount\": {\n \"type\": \"number\",\n \"description\": \"Number of words embedded\"\n },\n \"text\": {\n \"type\": \"string\",\n \"description\": \"Extracted text from the document\"\n }\n },\n \"required\": [\n \"fileId\",\n \"pageCount\",\n \"wordCount\",\n \"text\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.crop-regions` — Crop Regions\n\nRender a PDF or image and crop normalized bounding boxes into JPEG run output artifacts. Pair with transform.script (bbox lists from ai.parse layout) and ai.extract or ai.vision for captions.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/crop-regions`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | File input — template expression e.g. {{ input.document }} resolving to a PDF or image |\n| `regions` | string \\| array<object> | yes | | Regions to crop — pageIndex + normalized bbox per entry |\n| `renderScale` | number | no | `1` | Scale factor when rasterizing PDF pages before cropping |\n| `imageQuality` | integer | no | `85` | JPEG quality for cropped outputs |\n| `paddingFrac` | number | no | `0.02` | Padding around each bbox as a fraction of the shorter page edge |\n| `minCropPx` | integer | no | `8` | Minimum crop width/height in pixels; smaller crops fall back to the full page |\n| `maxRegions` | integer | no | `100` | Maximum regions processed per invocation |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `regions` | array<object> | yes | | Successfully cropped regions |\n| `skipped` | integer | no | | Regions skipped (invalid bbox or missing page) |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"description\": \"File input — template expression e.g. {{ input.document }} resolving to a PDF or image\"\n },\n \"regions\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Template expression resolving to a region array, e.g. {{ steps.list-figures.output.regions }}\"\n },\n {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"description\": \"Stable region id used in output and crop filenames (defaults to region-N)\",\n \"type\": \"string\"\n },\n \"pageIndex\": {\n \"description\": \"0-based page index to crop from (images use page 0)\",\n \"default\": 0,\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"bbox\": {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n }\n ],\n \"description\": \"Normalized axis-aligned bbox [x0, y0, x1, y1]\"\n }\n },\n \"required\": [\n \"bbox\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Regions to crop from the rendered document\"\n }\n ],\n \"description\": \"Regions to crop — pageIndex + normalized bbox per entry\"\n },\n \"renderScale\": {\n \"description\": \"Scale factor when rasterizing PDF pages before cropping\",\n \"default\": 1,\n \"type\": \"number\",\n \"minimum\": 0.5,\n \"maximum\": 4\n },\n \"imageQuality\": {\n \"description\": \"JPEG quality for cropped outputs\",\n \"default\": 85,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 100\n },\n \"paddingFrac\": {\n \"description\": \"Padding around each bbox as a fraction of the shorter page edge\",\n \"default\": 0.02,\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 0.2\n },\n \"minCropPx\": {\n \"description\": \"Minimum crop width/height in pixels; smaller crops fall back to the full page\",\n \"default\": 8,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991\n },\n \"maxRegions\": {\n \"description\": \"Maximum regions processed per invocation\",\n \"default\": 100,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 200\n }\n },\n \"required\": [\n \"input\",\n \"regions\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"regions\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"description\": \"Region id (from input or auto-generated)\"\n },\n \"pageIndex\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"0-based page index the crop was taken from\"\n },\n \"bbox\": {\n \"type\": \"array\",\n \"items\": [\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n }\n ],\n \"description\": \"Normalized bbox that was cropped\"\n },\n \"width\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991,\n \"description\": \"Crop width in pixels\"\n },\n \"height\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991,\n \"description\": \"Crop height in pixels\"\n },\n \"fileId\": {\n \"type\": \"string\",\n \"description\": \"Run output file id for the JPEG crop\"\n },\n \"filename\": {\n \"type\": \"string\",\n \"description\": \"Stored crop filename\"\n },\n \"mimeType\": {\n \"type\": \"string\",\n \"const\": \"image/jpeg\",\n \"description\": \"Always image/jpeg\"\n },\n \"size\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"JPEG size in bytes\"\n }\n },\n \"required\": [\n \"id\",\n \"pageIndex\",\n \"bbox\",\n \"width\",\n \"height\",\n \"fileId\",\n \"filename\",\n \"mimeType\",\n \"size\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Successfully cropped regions\"\n },\n \"skipped\": {\n \"description\": \"Regions skipped (invalid bbox or missing page)\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"regions\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.xlsx-to-json` — Spreadsheet to JSON\n\nConvert an XLS or XLSX spreadsheet to a JSON array of row objects. Supports headerless files, named or positional columns, displayed text, and a single rectangular range.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/xlsx-to-json`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string | yes | | File input - template expression e.g. {{input.document}} resolving to a scoped $file artifact at runtime |\n| `sheet` | integer \\| string | no | | Sheet to read: 0-based index or exact sheet name. Omit for the first sheet. |\n| `outputCsv` | boolean | no | `false` | If true, also write CSV to storage and include fileId. Zero-config uses the historical full-sheet SheetJS CSV. When columns, range, headerRow, valueMode, blankCells, or blankRows are set, CSV matches that projection. |\n| `includeMetadata` | boolean | no | `false` | If true, include sheet metadata and diagnostics in the step output. Omit to keep output as rows (and fileId when outputCsv is true). Warnings are still logged when this is false. |\n| `outputFilename` | string | no | | Output CSV filename when outputCsv is true - supports LiquidJS e.g. {{filename}}.csv |\n| `headerRow` | `false` \\| integer | no | | Header row: a positive 1-based Excel row, or false to keep the first effective row as data. Omit to use the first effective row as the header. When range is set and this is omitted, the first range row is the header. |\n| `columns` | array<object> | no | | Ordered output columns. Each item needs a key and exactly one source: index (0-based absolute column) or header (exact displayed header text). Named header sources require a header row. Omit to keep every column in the effective range. |\n| `valueMode` | `\"raw\"` \\| `\"displayed\"` | no | `\"raw\"` | raw (default) returns typed cached cell values. displayed returns formatted cell text (dates, leading zeros, punctuation, diacritics, embedded newlines). Formulas are never calculated. |\n| `range` | string | no | | Optional rectangular A1 range without a sheet qualifier, e.g. A1:D20. Disjoint ranges are rejected. |\n| `blankCells` | `\"empty-string\"` \\| `\"null\"` \\| `\"omit\"` | no | `\"empty-string\"` | How truly empty cells appear in each row object. Default empty-string. 0, false, and a formula that cached an empty string are not empty cells. |\n| `blankRows` | `\"skip\"` \\| `\"keep\"` | no | `\"skip\"` | skip (default) drops rows whose projected columns are all truly empty. keep retains them. Detection uses projected columns only. |\n| `limits` | object | no | | Optional workload caps that can only lower the server defaults. Omitted fields use the server defaults. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `rows` | array<record<string, unknown>> | yes | | Array of row objects (first row = headers as keys unless headerRow is false) |\n| `fileId` | string | no | | File ID of stored CSV when outputCsv is true |\n| `sheet` | object | no | | Selected sheet metadata after projection. Present only when includeMetadata is true. |\n| `diagnostics` | array<object> | no | | Non-fatal warnings collected while reading the sheet. Present only when includeMetadata is true. Warnings are still logged when metadata is omitted. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"File input - template expression e.g. {{input.document}} resolving to a scoped $file artifact at runtime\"\n },\n \"sheet\": {\n \"description\": \"Sheet to read: 0-based index or exact sheet name. Omit for the first sheet.\",\n \"anyOf\": [\n {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"outputCsv\": {\n \"description\": \"If true, also write CSV to storage and include fileId. Zero-config uses the historical full-sheet SheetJS CSV. When columns, range, headerRow, valueMode, blankCells, or blankRows are set, CSV matches that projection.\",\n \"default\": false,\n \"type\": \"boolean\"\n },\n \"includeMetadata\": {\n \"description\": \"If true, include sheet metadata and diagnostics in the step output. Omit to keep output as rows (and fileId when outputCsv is true). Warnings are still logged when this is false.\",\n \"default\": false,\n \"type\": \"boolean\"\n },\n \"outputFilename\": {\n \"description\": \"Output CSV filename when outputCsv is true - supports LiquidJS e.g. {{filename}}.csv\",\n \"type\": \"string\"\n },\n \"headerRow\": {\n \"description\": \"Header row: a positive 1-based Excel row, or false to keep the first effective row as data. Omit to use the first effective row as the header. When range is set and this is omitted, the first range row is the header.\",\n \"anyOf\": [\n {\n \"type\": \"boolean\",\n \"const\": false\n },\n {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991\n }\n ]\n },\n \"columns\": {\n \"description\": \"Ordered output columns. Each item needs a key and exactly one source: index (0-based absolute column) or header (exact displayed header text). Named header sources require a header row. Omit to keep every column in the effective range.\",\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"key\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Output object key for this column\"\n },\n \"index\": {\n \"description\": \"0-based absolute worksheet column. Use instead of header.\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"header\": {\n \"description\": \"Exact displayed header text. Use instead of index. Requires a header row.\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"occurrence\": {\n \"description\": \"1-based occurrence of header when the same displayed header appears more than once. Only valid with header.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"key\"\n ],\n \"additionalProperties\": false\n }\n },\n \"valueMode\": {\n \"description\": \"raw (default) returns typed cached cell values. displayed returns formatted cell text (dates, leading zeros, punctuation, diacritics, embedded newlines). Formulas are never calculated.\",\n \"default\": \"raw\",\n \"type\": \"string\",\n \"enum\": [\n \"raw\",\n \"displayed\"\n ]\n },\n \"range\": {\n \"description\": \"Optional rectangular A1 range without a sheet qualifier, e.g. A1:D20. Disjoint ranges are rejected.\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"blankCells\": {\n \"description\": \"How truly empty cells appear in each row object. Default empty-string. 0, false, and a formula that cached an empty string are not empty cells.\",\n \"default\": \"empty-string\",\n \"type\": \"string\",\n \"enum\": [\n \"empty-string\",\n \"null\",\n \"omit\"\n ]\n },\n \"blankRows\": {\n \"description\": \"skip (default) drops rows whose projected columns are all truly empty. keep retains them. Detection uses projected columns only.\",\n \"default\": \"skip\",\n \"type\": \"string\",\n \"enum\": [\n \"skip\",\n \"keep\"\n ]\n },\n \"limits\": {\n \"type\": \"object\",\n \"properties\": {\n \"maxInputBytes\": {\n \"description\": \"Maximum input file size in bytes. Omitted uses the server default (52,428,800, 50 MiB). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 52428800\n },\n \"maxRows\": {\n \"description\": \"Maximum rows in the selected range. Omitted uses the server default (100,000). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 100000\n },\n \"maxColumns\": {\n \"description\": \"Maximum columns in the selected range. Omitted uses the server default (256). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 256\n },\n \"maxCells\": {\n \"description\": \"Maximum cells in the selected range. Omitted uses the server default (5,000,000). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 5000000\n }\n },\n \"additionalProperties\": false,\n \"description\": \"Optional workload caps that can only lower the server defaults. Omitted fields use the server defaults.\"\n }\n },\n \"required\": [\n \"input\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"rows\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"description\": \"Array of row objects (first row = headers as keys unless headerRow is false)\"\n },\n \"fileId\": {\n \"description\": \"File ID of stored CSV when outputCsv is true\",\n \"type\": \"string\"\n },\n \"sheet\": {\n \"description\": \"Selected sheet metadata after projection. Present only when includeMetadata is true.\",\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Selected sheet name\"\n },\n \"index\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"0-based sheet index in the workbook\"\n },\n \"range\": {\n \"type\": \"string\",\n \"description\": \"Effective rectangular A1 range that was read\"\n },\n \"rowCount\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Number of data rows returned in rows\"\n },\n \"columns\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Output keys in order\"\n }\n },\n \"required\": [\n \"name\",\n \"index\",\n \"range\",\n \"rowCount\",\n \"columns\"\n ],\n \"additionalProperties\": false\n },\n \"diagnostics\": {\n \"description\": \"Non-fatal warnings collected while reading the sheet. Present only when includeMetadata is true. Warnings are still logged when metadata is omitted.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"severity\": {\n \"type\": \"string\",\n \"const\": \"warning\",\n \"description\": \"Diagnostic severity\"\n },\n \"code\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Stable diagnostic code\"\n },\n \"message\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Human-readable diagnostic message\"\n },\n \"cell\": {\n \"description\": \"A1 address of the related cell when applicable\",\n \"type\": \"string\"\n },\n \"row\": {\n \"description\": \"1-based Excel row when applicable\",\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n },\n \"column\": {\n \"description\": \"0-based worksheet column when applicable\",\n \"type\": \"integer\",\n \"minimum\": -9007199254740991,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"severity\",\n \"code\",\n \"message\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"rows\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.json-to-xlsx` — JSON to XLSX\n\nConvert JSON rows into an XLSX spreadsheet. Pair with transform.xlsx-to-json. Formula-looking strings stay text; nested cell values are rejected.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/json-to-xlsx`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `filename` | string | no | | Output filename — supports LiquidJS; .xlsx is added if omitted |\n| `columns` | array<object> | no | | Ordered columns for a single-sheet workbook. Use sheets for multiple sheets. |\n| `rows` | string \\| array<record<string, unknown>> | no | | Array of row objects, or a template expression that resolves to one |\n| `sheets` | array<object> | no | | Multiple sheets. Do not combine with top-level columns/rows. |\n| `limits` | object | no | | Optional workload caps that can only lower the server defaults. Omitted fields use the server defaults. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `fileId` | string | yes | | File ID from the files table |\n| `filename` | string | yes | | Sanitized output filename including .xlsx |\n| `sheetCount` | integer | yes | | Number of sheets in the workbook |\n| `sheets` | array<object> | yes | | Per-sheet name and row count |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"filename\": {\n \"description\": \"Output filename — supports LiquidJS; .xlsx is added if omitted\",\n \"type\": \"string\"\n },\n \"columns\": {\n \"description\": \"Ordered columns for a single-sheet workbook. Use sheets for multiple sheets.\",\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"key\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Row object key to read for this column\"\n },\n \"header\": {\n \"description\": \"Header cell text. Defaults to key.\",\n \"type\": \"string\"\n },\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"string\",\n \"number\",\n \"boolean\",\n \"date\"\n ],\n \"description\": \"Cell type. Omit to infer from the JSON value. YYYY-MM-DD strings stay text unless type is date.\"\n }\n },\n \"required\": [\n \"key\"\n ],\n \"additionalProperties\": false\n }\n },\n \"rows\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Template expression resolving to an array of row objects, e.g. {{ steps.extract.output.items }}\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"description\": \"Array of row objects\"\n }\n ],\n \"description\": \"Array of row objects, or a template expression that resolves to one\"\n },\n \"sheets\": {\n \"description\": \"Multiple sheets. Do not combine with top-level columns/rows.\",\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Sheet name. Defaults to Sheet1, Sheet2, …. Excel limit: 31 characters. Cannot contain : \\\\ / ? * [ ], cannot start or end with a single quote, and History is reserved.\"\n },\n \"columns\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"key\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Row object key to read for this column\"\n },\n \"header\": {\n \"description\": \"Header cell text. Defaults to key.\",\n \"type\": \"string\"\n },\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"string\",\n \"number\",\n \"boolean\",\n \"date\"\n ],\n \"description\": \"Cell type. Omit to infer from the JSON value. YYYY-MM-DD strings stay text unless type is date.\"\n }\n },\n \"required\": [\n \"key\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Ordered columns for this sheet\"\n },\n \"rows\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Template expression resolving to an array of row objects, e.g. {{ steps.extract.output.items }}\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"description\": \"Array of row objects\"\n }\n ],\n \"description\": \"Array of row objects, or a template expression that resolves to one\"\n }\n },\n \"required\": [\n \"columns\",\n \"rows\"\n ],\n \"additionalProperties\": false\n }\n },\n \"limits\": {\n \"type\": \"object\",\n \"properties\": {\n \"maxSheets\": {\n \"description\": \"Maximum worksheets. Omitted uses the server default (50). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 50\n },\n \"maxRowsPerSheet\": {\n \"description\": \"Maximum data rows per sheet. Omitted uses the server default (100,000). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 100000\n },\n \"maxTotalRows\": {\n \"description\": \"Maximum data rows summed across all sheets. Omitted uses the server default (250,000). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 250000\n },\n \"maxColumns\": {\n \"description\": \"Maximum columns per sheet. Omitted uses the server default (256). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 256\n },\n \"maxTotalCells\": {\n \"description\": \"Maximum populated cells across the workbook. Omitted uses the server default (5,000,000). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 5000000\n },\n \"maxOutputBytes\": {\n \"description\": \"Maximum serialized .xlsx size in bytes. Omitted uses the server default (52,428,800, 50 MiB). Cannot exceed it.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 52428800\n }\n },\n \"additionalProperties\": false,\n \"description\": \"Optional workload caps that can only lower the server defaults. Omitted fields use the server defaults.\"\n }\n },\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"fileId\": {\n \"type\": \"string\",\n \"description\": \"File ID from the files table\"\n },\n \"filename\": {\n \"type\": \"string\",\n \"description\": \"Sanitized output filename including .xlsx\"\n },\n \"sheetCount\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 9007199254740991,\n \"description\": \"Number of sheets in the workbook\"\n },\n \"sheets\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"description\": \"Written sheet name\"\n },\n \"rowCount\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Number of data rows written (excludes header)\"\n }\n },\n \"required\": [\n \"name\",\n \"rowCount\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Per-sheet name and row count\"\n }\n },\n \"required\": [\n \"fileId\",\n \"filename\",\n \"sheetCount\",\n \"sheets\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.script` — Script\n\nExecute a TypeScript function in a QuickJS sandbox. Input keys become function parameters in any order, and the required `: R` return-type annotation IS this step's output schema: `inputs: { items, taxRate }` accepts `function script(taxRate: …, items: …): R { … }`.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/script`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `inputs` | record<string, string> | no | | Named inputs mapped from template expressions. Keys become function parameters; parameter order is independent of YAML/JSON key order: `inputs: { items, taxRate }` accepts `function script(taxRate: …, items: …): R { … }`. |\n| `function` | string | yes | | TypeScript function declaration. Must be `function script(args): R { … }` where parameter names exactly match the configured input keys in any order and `R` is a return type annotation. The annotation IS this step's output schema. |\n| `timeout` | number | no | `5000` | Max execution time in milliseconds (default: 5000) |\n| `memoryLimit` | number | no | `10485760` | Max memory in bytes (default: 10MB) |\n\n**Output:** `unknown`\n\n> Value returned from script. Validated at runtime against the JSON Schema derived from the function's return type annotation.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"inputs\": {\n \"description\": \"Named inputs mapped from template expressions. Keys become function parameters; parameter order is independent of YAML/JSON key order: `inputs: { items, taxRate }` accepts `function script(taxRate: …, items: …): R { … }`.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"string\"\n }\n },\n \"function\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 50000,\n \"description\": \"TypeScript function declaration. Must be `function script(args): R { … }` where parameter names exactly match the configured input keys in any order and `R` is a return type annotation. The annotation IS this step's output schema.\"\n },\n \"timeout\": {\n \"description\": \"Max execution time in milliseconds (default: 5000)\",\n \"default\": 5000,\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"memoryLimit\": {\n \"description\": \"Max memory in bytes (default: 10MB)\",\n \"default\": 10485760,\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n }\n },\n \"required\": [\n \"function\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"description\": \"Value returned from script. Validated at runtime against the JSON Schema derived from the function's return type annotation.\"\n}\n```\n\n\n#### `transform.text-chunker` — Text Chunker\n\nSplit long text into chunks with regex-anchored boundaries, overlap, and header preservation. Accepts raw text or a parsed-document object; chunks carry source page indexes when pages are provided.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/text-chunker`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string \\| record<string, unknown> | yes | | Either raw text or a parsed-document object `{ pages: [{ pageIndex, text }] }` (e.g. `{{ steps.parse.output }}`). Pages preserve per-chunk page provenance. |\n| `maxChars` | integer | yes | | Target chunk size in characters. Hard ceiling per chunk is 1.5×. |\n| `overlap` | integer | no | `0` | Characters duplicated at chunk boundaries (default 0). Must be < maxChars / 2. |\n| `splitOn` | array<string> | no | | Ordered list of regexes; the first that matches near the chunk boundary wins. Falls back to char-cut when none match. Tip: list narrowest first (e.g. /\\d+\\.\\d+\\s+/ before /\\n\\n+/). |\n| `maxChunks` | integer | no | `64` | Safety cap; later chunks are dropped and `summary.truncated` flips to true. |\n| `preserveHeader` | integer | no | `0` | Prepend the first N characters of the input to every chunk (good for \"always include the contract title\"). |\n| `minChunkChars` | integer | no | `0` | Trailing chunks shorter than this are merged into the previous chunk. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `chunks` | array<object> | yes | | |\n| `summary` | object | yes | | |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"pages\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"text\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"pageIndex\",\n \"text\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"pages\"\n ],\n \"additionalProperties\": {}\n }\n ],\n \"description\": \"Either raw text or a parsed-document object `{ pages: [{ pageIndex, text }] }` (e.g. `{{ steps.parse.output }}`). Pages preserve per-chunk page provenance.\"\n },\n \"maxChars\": {\n \"type\": \"integer\",\n \"minimum\": 100,\n \"maximum\": 100000,\n \"description\": \"Target chunk size in characters. Hard ceiling per chunk is 1.5×.\"\n },\n \"overlap\": {\n \"default\": 0,\n \"description\": \"Characters duplicated at chunk boundaries (default 0). Must be < maxChars / 2.\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"splitOn\": {\n \"description\": \"Ordered list of regexes; the first that matches near the chunk boundary wins. Falls back to char-cut when none match. Tip: list narrowest first (e.g. /\\\\d+\\\\.\\\\d+\\\\s+/ before /\\\\n\\\\n+/).\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"maxChunks\": {\n \"default\": 64,\n \"description\": \"Safety cap; later chunks are dropped and `summary.truncated` flips to true.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 1000\n },\n \"preserveHeader\": {\n \"default\": 0,\n \"description\": \"Prepend the first N characters of the input to every chunk (good for \\\"always include the contract title\\\").\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"minChunkChars\": {\n \"default\": 0,\n \"description\": \"Trailing chunks shorter than this are merged into the previous chunk.\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"input\",\n \"maxChars\",\n \"overlap\",\n \"maxChunks\",\n \"preserveHeader\",\n \"minChunkChars\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"chunks\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"text\": {\n \"type\": \"string\"\n },\n \"index\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"total\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"startOffset\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"endOffset\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"pages\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"description\": \"Page indexes that contributed text to this chunk (empty for raw-string input).\"\n }\n },\n \"required\": [\n \"text\",\n \"index\",\n \"total\",\n \"startOffset\",\n \"endOffset\",\n \"pages\"\n ],\n \"additionalProperties\": false\n }\n },\n \"summary\": {\n \"type\": \"object\",\n \"properties\": {\n \"inputChars\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"totalChunks\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"avgChunkChars\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"maxChunkChars\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"truncated\": {\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"inputChars\",\n \"totalChunks\",\n \"avgChunkChars\",\n \"maxChunkChars\",\n \"truncated\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"chunks\",\n \"summary\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.regex-extract` — Regex Extract\n\nPull named fields from text via regex patterns (deterministic counterpart to ai.extract). Accepts raw text or a parsed-document object; matches carry `_evidence.pageIndex` when pages are provided.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/regex-extract`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `input` | string \\| record<string, unknown> | yes | | Either raw text or a parsed-document object `{ pages: [{ pageIndex, text }] }`. Pages enable per-match `_evidence.pageIndex`. |\n| `fields` | record<string, object> | yes | | Named field → pattern mapping. |\n| `flags` | string | no | | Default regex flags applied when a field omits its own `flags`. Subset of \"gimsuy\". |\n| `searchWindow` | integer | no | | Only search the first N characters of input (perf). Omit for full search. |\n\n**Output:** `record<string, unknown>`\n\n> Field name → extracted value (or default), plus `_evidence: { [field]: { pageIndex, matchOffset, raw } }` and `_unmatched: string[]`.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"input\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"pages\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"pageIndex\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"text\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"pageIndex\",\n \"text\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"pages\"\n ],\n \"additionalProperties\": {}\n }\n ],\n \"description\": \"Either raw text or a parsed-document object `{ pages: [{ pageIndex, text }] }`. Pages enable per-match `_evidence.pageIndex`.\"\n },\n \"fields\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"pattern\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Regex source. Must contain at least one capturing group.\"\n },\n \"flags\": {\n \"description\": \"Per-field regex flags (overrides the step-level `flags`). Subset of \\\"gimsuy\\\".\",\n \"type\": \"string\",\n \"pattern\": \"^(?!.*(.).*\\\\1)[gimsuy]*$\"\n },\n \"max\": {\n \"default\": 1,\n \"description\": \"Max matches to keep. Default 1. Use \\\"all\\\" plus `select` to combine multiple hits.\",\n \"anyOf\": [\n {\n \"type\": \"number\",\n \"const\": 1\n },\n {\n \"type\": \"string\",\n \"const\": \"all\"\n },\n {\n \"type\": \"integer\",\n \"exclusiveMinimum\": 0,\n \"maximum\": 9007199254740991\n }\n ]\n },\n \"select\": {\n \"description\": \"When max ≠ 1: which match to surface. \\\"first\\\" (default), \\\"last\\\", \\\"min\\\", \\\"max\\\", or \\\"all\\\" — `all` returns the full array of matches AND a parallel array of evidence entries.\",\n \"type\": \"string\",\n \"enum\": [\n \"first\",\n \"last\",\n \"min\",\n \"max\",\n \"all\"\n ]\n },\n \"normalize\": {\n \"type\": \"string\",\n \"enum\": [\n \"strip-spaces\",\n \"strip-punct\",\n \"collapse-whitespace\",\n \"lower\",\n \"upper\",\n \"trim\"\n ]\n },\n \"format\": {\n \"description\": \"Post-processing. iso-date accepts DD.MM.YYYY (1 group) or DD/MM/YYYY split across 3 groups → \\\"YYYY-MM-DD\\\".\",\n \"type\": \"string\",\n \"enum\": [\n \"iso-date\",\n \"iso-number\",\n \"lower\",\n \"upper\",\n \"trim\"\n ]\n },\n \"exclude\": {\n \"description\": \"Post-normalize values to skip (known wrong matches).\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"default\": {\n \"description\": \"Value when nothing matches.\",\n \"type\": \"string\"\n },\n \"contextChars\": {\n \"description\": \"When > 0, evidence entries gain `contextBefore` and `contextAfter` snippets of N characters surrounding the match. Useful for UI/debug display.\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 2000\n }\n },\n \"required\": [\n \"pattern\",\n \"max\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Named field → pattern mapping.\"\n },\n \"flags\": {\n \"description\": \"Default regex flags applied when a field omits its own `flags`. Subset of \\\"gimsuy\\\".\",\n \"type\": \"string\",\n \"pattern\": \"^(?!.*(.).*\\\\1)[gimsuy]*$\"\n },\n \"searchWindow\": {\n \"description\": \"Only search the first N characters of input (perf). Omit for full search.\",\n \"type\": \"integer\",\n \"minimum\": 100,\n \"maximum\": 9007199254740991\n }\n },\n \"required\": [\n \"input\",\n \"fields\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Field name → extracted value (or default), plus `_evidence: { [field]: { pageIndex, matchOffset, raw } }` and `_unmatched: string[]`.\"\n}\n```\n\n\n#### `transform.archive-list` — List Archive\n\nList files inside a ZIP archive. Returns sorted normalized paths with compressed and uncompressed sizes. Optionally filter by folder prefix or substring, then page with offset and limit. ZIP is the only accepted format in this version. Pair with foreach or parallel_map and transform.archive-extract to open individual files.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/archive-list`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `archive` | string | yes | | Template expression or file reference for the ZIP archive to list. ZIP is the only accepted format in this version. |\n| `prefix` | string | no | | Optional folder prefix inside the archive after path normalization (for example invoices or invoices/). |\n| `search` | string | no | | Optional case-insensitive substring match on the normalized archive path. |\n| `offset` | integer | no | | Number of matching entries to skip after sorting. Default 0. |\n| `limit` | integer | no | | Maximum matching entries to return after sorting. Capped at 10000. Omit to return every matching entry up to that cap. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `entries` | array<object> | yes | | Matching files, sorted by normalized path. Directories and junk paths are omitted. |\n| `total` | integer | yes | | Number of matching entries before offset/limit pagination. |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"archive\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Template expression or file reference for the ZIP archive to list. ZIP is the only accepted format in this version.\"\n },\n \"prefix\": {\n \"description\": \"Optional folder prefix inside the archive after path normalization (for example invoices or invoices/).\",\n \"type\": \"string\"\n },\n \"search\": {\n \"description\": \"Optional case-insensitive substring match on the normalized archive path.\",\n \"type\": \"string\"\n },\n \"offset\": {\n \"description\": \"Number of matching entries to skip after sorting. Default 0.\",\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"limit\": {\n \"description\": \"Maximum matching entries to return after sorting. Capped at 10000. Omit to return every matching entry up to that cap.\",\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10000\n }\n },\n \"required\": [\n \"archive\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"entries\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"path\": {\n \"type\": \"string\",\n \"description\": \"Normalized archive-relative path\"\n },\n \"compressedSize\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Declared compressed size in bytes\"\n },\n \"uncompressedSize\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Declared uncompressed size in bytes\"\n }\n },\n \"required\": [\n \"path\",\n \"compressedSize\",\n \"uncompressedSize\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Matching files, sorted by normalized path. Directories and junk paths are omitted.\"\n },\n \"total\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Number of matching entries before offset/limit pagination.\"\n }\n },\n \"required\": [\n \"entries\",\n \"total\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `transform.archive-extract` — Extract Archive File\n\nExtract exactly one file from a ZIP archive and store it as a run output file. Downstream steps can pass {{ steps.extract.output }} into Parse, Vision, or another archive step. Nested ZIPs are stored as ZIP files, not unpacked. Each extraction is capped at 64 MiB. ZIP is the only accepted format in this version.\n\n**Behavior and examples:** `eigenpal docs read steps/transform/archive-extract`\n\n**Durable retry:** Transforms, including those that write files, are not durably retried.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `archive` | string | yes | | Template expression or file reference for the ZIP archive. ZIP is the only accepted format in this version. |\n| `path` | string | yes | | Archive-relative path of the single file to extract, for example {{ item.path }} from a prior archive-list step. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `fileId` | string | yes | | File ID from the files table |\n| `path` | string | yes | | Normalized archive-relative path that was extracted |\n| `filename` | string | yes | | Basename of the extracted file |\n| `mimeType` | string | yes | | Detected MIME type of the extracted file |\n| `size` | integer | yes | | Uncompressed size in bytes |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"archive\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Template expression or file reference for the ZIP archive. ZIP is the only accepted format in this version.\"\n },\n \"path\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Archive-relative path of the single file to extract, for example {{ item.path }} from a prior archive-list step.\"\n }\n },\n \"required\": [\n \"archive\",\n \"path\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"fileId\": {\n \"type\": \"string\",\n \"description\": \"File ID from the files table\"\n },\n \"path\": {\n \"type\": \"string\",\n \"description\": \"Normalized archive-relative path that was extracted\"\n },\n \"filename\": {\n \"type\": \"string\",\n \"description\": \"Basename of the extracted file\"\n },\n \"mimeType\": {\n \"type\": \"string\",\n \"description\": \"Detected MIME type of the extracted file\"\n },\n \"size\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Uncompressed size in bytes\"\n }\n },\n \"required\": [\n \"fileId\",\n \"path\",\n \"filename\",\n \"mimeType\",\n \"size\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n### Action steps — external side effects\n\n#### `action.http` — HTTP Request\n\nMake an HTTP request to an external API\n\n**Behavior and examples:** `eigenpal docs read steps/action/http`\n\n**Durable retry:** HTTP `GET` and `HEAD` can durably retry transient timeout, rate limits, selected retryable server failures failures. Other methods are not replayed.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `url` | string | yes | | Request URL (supports template expressions) |\n| `method` | `\"GET\"` \\| `\"HEAD\"` \\| `\"POST\"` \\| `\"PUT\"` \\| `\"PATCH\"` \\| `\"DELETE\"` | no | `\"GET\"` | |\n| `headers` | record<string, string> | no | | HTTP headers |\n| `body` | unknown | no | | Request body (JSON or string) |\n| `timeout` | number | no | `30000` | Timeout in milliseconds |\n| `insecureSkipTlsVerify` | boolean | no | `false` | If true, skip TLS certificate verification (use only for read-only public endpoints with bad/expired certs) |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `status` | number | yes | | HTTP status code |\n| `statusText` | string | yes | | |\n| `headers` | record<string, string> | yes | | |\n| `body` | unknown | yes | | Response body (parsed JSON or string) |\n| `responseCharset` | string | yes | | Charset used to decode the response body (e.g. utf-8, windows-1250) |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"url\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Request URL (supports template expressions)\"\n },\n \"method\": {\n \"default\": \"GET\",\n \"type\": \"string\",\n \"enum\": [\n \"GET\",\n \"HEAD\",\n \"POST\",\n \"PUT\",\n \"PATCH\",\n \"DELETE\"\n ]\n },\n \"headers\": {\n \"description\": \"HTTP headers\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"string\"\n }\n },\n \"body\": {\n \"description\": \"Request body (JSON or string)\"\n },\n \"timeout\": {\n \"description\": \"Timeout in milliseconds\",\n \"default\": 30000,\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"insecureSkipTlsVerify\": {\n \"description\": \"If true, skip TLS certificate verification (use only for read-only public endpoints with bad/expired certs)\",\n \"default\": false,\n \"type\": \"boolean\"\n }\n },\n \"required\": [\n \"url\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"number\",\n \"description\": \"HTTP status code\"\n },\n \"statusText\": {\n \"type\": \"string\"\n },\n \"headers\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"string\"\n }\n },\n \"body\": {\n \"description\": \"Response body (parsed JSON or string)\"\n },\n \"responseCharset\": {\n \"type\": \"string\",\n \"description\": \"Charset used to decode the response body (e.g. utf-8, windows-1250)\"\n }\n },\n \"required\": [\n \"status\",\n \"statusText\",\n \"headers\",\n \"body\",\n \"responseCharset\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `action.invoke-workflow` — Invoke Workflow\n\nExecute another workflow and return its output\n\n**Behavior and examples:** `eigenpal docs read steps/action/invoke-workflow`\n\n**Durable retry:** Invoked workflows are not replayed as durable leaf attempts.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `workflow` | string | no | | Workflow to invoke — definition name or wf_ id (tenant-scoped) |\n| `workflowId` | string | no | | Legacy alias for workflow when the value is a wf_ id |\n| `execution` | `\"inline\"` \\| `\"child\"` | no | | inline: run target steps in this execution (default). child: spawn a separate run with lineage. |\n| `input` | record<string, unknown> | no | | Input record keyed by the invoked workflow's declared inputs |\n| `wait` | boolean | no | | Child mode only. Wait for the invoked workflow to complete and return its output (default: true). Set false for fire-and-forget. |\n| `timeout` | number | no | | Child mode only. Max wait time in ms while the child is queued or running (default: 300000). Once the child pauses for human review, the parent pauses durably instead of using this timeout. |\n| `pollInterval` | number | no | | Child mode only. How often to poll status in ms when waiting (default: 1000) |\n\n**Output:** `record<string, unknown>`\n\n> When wait is true, the invoked workflow's declared output fields are flattened to the top level alongside a `files` array — reference them as {{ steps.<invoke>.output.<field> }} (there is no `.data` or `.result` wrapper). When wait is false, execution metadata. CLI authors can run `eigenpal workflow schema <workflow-id>` to inspect resolved fields; Studio builder agents can call `get_workflow_output_schema`.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"workflow\": {\n \"description\": \"Workflow to invoke — definition name or wf_ id (tenant-scoped)\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"workflowId\": {\n \"description\": \"Legacy alias for workflow when the value is a wf_ id\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"execution\": {\n \"description\": \"inline: run target steps in this execution (default). child: spawn a separate run with lineage.\",\n \"type\": \"string\",\n \"enum\": [\n \"inline\",\n \"child\"\n ]\n },\n \"input\": {\n \"description\": \"Input record keyed by the invoked workflow's declared inputs\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"wait\": {\n \"description\": \"Child mode only. Wait for the invoked workflow to complete and return its output (default: true). Set false for fire-and-forget.\",\n \"type\": \"boolean\"\n },\n \"timeout\": {\n \"description\": \"Child mode only. Max wait time in ms while the child is queued or running (default: 300000). Once the child pauses for human review, the parent pauses durably instead of using this timeout.\",\n \"type\": \"number\"\n },\n \"pollInterval\": {\n \"description\": \"Child mode only. How often to poll status in ms when waiting (default: 1000)\",\n \"type\": \"number\"\n }\n },\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"When wait is true, the invoked workflow's declared output fields are flattened to the top level alongside a `files` array — reference them as {{ steps.<invoke>.output.<field> }} (there is no `.data` or `.result` wrapper). When wait is false, execution metadata. CLI authors can run `eigenpal workflow schema <workflow-id>` to inspect resolved fields; Studio builder agents can call `get_workflow_output_schema`.\"\n}\n```\n\n\n#### `action.website-reader` — Website Reader\n\nFetch a webpage and convert content to markdown\n\n**Behavior and examples:** `eigenpal docs read steps/action/website-reader`\n\n**Durable retry:** Supported for transient timeout, rate limits, selected retryable server failures failures.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `url` | string | yes | | Website URL to fetch (supports template expressions) |\n| `timeout` | number | no | `30000` | Timeout in milliseconds |\n| `encoding` | `\"auto\"` \\| `\"utf-8\"` \\| `\"latin1\"` \\| `\"windows-1250\"` \\| `\"windows-1252\"` \\| `\"iso-8859-2\"` | no | `\"auto\"` | Response encoding. Auto detects from Content-Type header and HTML meta tags. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `markdown` | string | yes | | Page content converted to markdown |\n| `title` | string \\| null | yes | | Page title |\n| `excerpt` | string \\| null | yes | | Page excerpt/description |\n| `byline` | string \\| null | yes | | Author information |\n| `siteName` | string \\| null | yes | | Site name |\n| `length` | number | yes | | Content length in characters |\n| `url` | string | yes | | Final URL after redirects |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"url\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Website URL to fetch (supports template expressions)\"\n },\n \"timeout\": {\n \"description\": \"Timeout in milliseconds\",\n \"default\": 30000,\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"encoding\": {\n \"description\": \"Response encoding. Auto detects from Content-Type header and HTML meta tags.\",\n \"default\": \"auto\",\n \"type\": \"string\",\n \"enum\": [\n \"auto\",\n \"utf-8\",\n \"latin1\",\n \"windows-1250\",\n \"windows-1252\",\n \"iso-8859-2\"\n ]\n }\n },\n \"required\": [\n \"url\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"markdown\": {\n \"type\": \"string\",\n \"description\": \"Page content converted to markdown\"\n },\n \"title\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"Page title\"\n },\n \"excerpt\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"Page excerpt/description\"\n },\n \"byline\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"Author information\"\n },\n \"siteName\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"Site name\"\n },\n \"length\": {\n \"type\": \"number\",\n \"description\": \"Content length in characters\"\n },\n \"url\": {\n \"type\": \"string\",\n \"description\": \"Final URL after redirects\"\n }\n },\n \"required\": [\n \"markdown\",\n \"title\",\n \"excerpt\",\n \"byline\",\n \"siteName\",\n \"length\",\n \"url\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `action.email` — Send Email\n\nSend an email with optional attachments via a selected email server. At most 10 recipients combined across to, cc, and bcc.\n\n**Behavior and examples:** `eigenpal docs read steps/action/email`\n\n**Durable retry:** This control step is not retried durably.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `server` | string | yes | | Outbound email server id (`ems_…`) |\n| `to` | string \\| array<string> | yes | | Primary recipient(s). String, comma-separated list, or nonempty string array. Combined with cc and bcc, at most 10 recipients. |\n| `cc` | string \\| array<string> | no | | Carbon-copy recipient(s). String, comma-separated list, or string array. Combined with to and bcc, at most 10 recipients. |\n| `bcc` | string \\| array<string> | no | | Blind carbon-copy recipient(s). String, comma-separated list, or string array. Combined with to and cc, at most 10 recipients. |\n| `replyTo` | string | no | | Reply-To address (supports template expressions) |\n| `subject` | string | yes | | Email subject |\n| `text` | string | yes | | Plain-text email body (max 50KiB) |\n| `attachments` | array<string \\| object> | no | | Optional same-run attachments as { fileId, filename? } or a bare fileId string (12MiB combined) |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `serverId` | string | yes | | Email server that sent (or would have sent) the message |\n| `messageId` | string \\| null | yes | | Provider message id. Null when delivery was skipped (evaluation runs). |\n| `provider` | `\"resend\"` \\| `\"smtp\"` \\| null | yes | | Email provider that sent the message. Null when delivery was skipped. |\n| `to` | array<string> | yes | | Normalized To recipients |\n| `cc` | array<string> | yes | | Normalized Cc recipients |\n| `bcc` | array<string> | yes | | Normalized Bcc recipients |\n| `attachmentCount` | integer | yes | | Number of attachments sent |\n| `totalBytes` | integer | yes | | Total attachment size in bytes |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"server\": {\n \"type\": \"string\",\n \"maxLength\": 128,\n \"pattern\": \"^ems_[A-Za-z0-9_-]+$\",\n \"description\": \"Outbound email server id (`ems_…`)\"\n },\n \"to\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1\n },\n {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n }\n ],\n \"description\": \"Primary recipient(s). String, comma-separated list, or nonempty string array. Combined with cc and bcc, at most 10 recipients.\"\n },\n \"cc\": {\n \"description\": \"Carbon-copy recipient(s). String, comma-separated list, or string array. Combined with to and bcc, at most 10 recipients.\",\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n }\n ]\n },\n \"bcc\": {\n \"description\": \"Blind carbon-copy recipient(s). String, comma-separated list, or string array. Combined with to and cc, at most 10 recipients.\",\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n }\n ]\n },\n \"replyTo\": {\n \"description\": \"Reply-To address (supports template expressions)\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"subject\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Email subject\"\n },\n \"text\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Plain-text email body (max 50KiB)\"\n },\n \"attachments\": {\n \"description\": \"Optional same-run attachments as { fileId, filename? } or a bare fileId string (12MiB combined)\",\n \"maxItems\": 10,\n \"type\": \"array\",\n \"items\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"File id or template expression that resolves to a file id\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"fileId\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"File id or template expression that resolves to a file id\"\n },\n \"filename\": {\n \"description\": \"Optional download filename override\",\n \"type\": \"string\",\n \"minLength\": 1\n }\n },\n \"required\": [\n \"fileId\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n }\n },\n \"required\": [\n \"server\",\n \"to\",\n \"subject\",\n \"text\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"serverId\": {\n \"type\": \"string\",\n \"maxLength\": 128,\n \"pattern\": \"^ems_[A-Za-z0-9_-]+$\",\n \"description\": \"Email server that sent (or would have sent) the message\"\n },\n \"messageId\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"Provider message id. Null when delivery was skipped (evaluation runs).\"\n },\n \"provider\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"enum\": [\n \"resend\",\n \"smtp\"\n ]\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"Email provider that sent the message. Null when delivery was skipped.\"\n },\n \"to\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Normalized To recipients\"\n },\n \"cc\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Normalized Cc recipients\"\n },\n \"bcc\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"Normalized Bcc recipients\"\n },\n \"attachmentCount\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Number of attachments sent\"\n },\n \"totalBytes\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991,\n \"description\": \"Total attachment size in bytes\"\n }\n },\n \"required\": [\n \"serverId\",\n \"messageId\",\n \"provider\",\n \"to\",\n \"cc\",\n \"bcc\",\n \"attachmentCount\",\n \"totalBytes\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n### Control steps — flow control\n\n#### `control.if` — Condition\n\nBranch execution based on a condition expression\n\n**Behavior and examples:** `eigenpal docs read steps/control/if`\n\n**Durable retry:** The control container itself is not retried, but eligible leaves inside its sequential scope may retry durably.\n\n**Config** (at step level):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `condition` | string | yes | | LiquidJS expression that evaluates to boolean |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `condition` | boolean | yes | | Evaluated condition result |\n| `branch` | `\"then\"` \\| `\"else\"` | yes | | Which branch was executed |\n| `result` | unknown | yes | | Output from executed branch |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"condition\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"LiquidJS expression that evaluates to boolean\"\n }\n },\n \"required\": [\n \"condition\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"condition\": {\n \"type\": \"boolean\",\n \"description\": \"Evaluated condition result\"\n },\n \"branch\": {\n \"type\": \"string\",\n \"enum\": [\n \"then\",\n \"else\"\n ],\n \"description\": \"Which branch was executed\"\n },\n \"result\": {\n \"description\": \"Output from executed branch\"\n }\n },\n \"required\": [\n \"condition\",\n \"branch\",\n \"result\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `control.switch` — Switch\n\nMulti-way routing: resolve an expression and run the first case whose value matches (else default). Cleaner than a nested control.if chain for routing an item to one of N pipelines by a discriminator field like a document type.\n\n**Behavior and examples:** `eigenpal docs read steps/control/switch`\n\n**Durable retry:** The control container itself is not retried, but eligible leaves inside its sequential scope may retry durably.\n\n**Config** (at step level):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `on` | string | yes | | Template expression whose resolved value selects the case, e.g. \"{{ doc.type }}\". |\n| `cases` | array<object> | yes | | Ordered cases; the first whose `when` matches runs. Each case has its own `steps`. |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `matched` | string \\| number \\| boolean \\| null | yes | | The `when` value that matched, or null when the default (or no) branch ran. |\n| `branch` | `\"case\"` \\| `\"default\"` \\| `\"none\"` | yes | | Which branch executed. |\n| `result` | unknown | yes | | Output from the executed branch (its last step). |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"on\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Template expression whose resolved value selects the case, e.g. \\\"{{ doc.type }}\\\".\"\n },\n \"cases\": {\n \"minItems\": 1,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"when\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"boolean\"\n }\n ],\n \"description\": \"Value matched (string-compared) against the resolved `on` value.\"\n }\n },\n \"required\": [\n \"when\"\n ],\n \"additionalProperties\": false\n },\n \"description\": \"Ordered cases; the first whose `when` matches runs. Each case has its own `steps`.\"\n }\n },\n \"required\": [\n \"on\",\n \"cases\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"matched\": {\n \"anyOf\": [\n {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"boolean\"\n }\n ]\n },\n {\n \"type\": \"null\"\n }\n ],\n \"description\": \"The `when` value that matched, or null when the default (or no) branch ran.\"\n },\n \"branch\": {\n \"type\": \"string\",\n \"enum\": [\n \"case\",\n \"default\",\n \"none\"\n ],\n \"description\": \"Which branch executed.\"\n },\n \"result\": {\n \"description\": \"Output from the executed branch (its last step).\"\n }\n },\n \"required\": [\n \"matched\",\n \"branch\",\n \"result\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `control.foreach` — For Each\n\nLoop over an array and execute steps for each item\n\n**Behavior and examples:** `eigenpal docs read steps/control/foreach`\n\n**Durable retry:** The control container itself is not retried, but eligible leaves inside its sequential scope may retry durably.\n\n**Config** (at step level):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `items` | string | yes | | Expression resolving to array |\n| `as` | string | yes | | Variable name for current item |\n| `indexAs` | string | no | | Variable name for current index |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `items` | array<unknown> | yes | | Results from each iteration |\n| `count` | number | yes | | Number of completed iterations |\n| `totalIterations` | number | yes | | Total iterations |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Expression resolving to array\"\n },\n \"as\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Variable name for current item\"\n },\n \"indexAs\": {\n \"description\": \"Variable name for current index\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"items\",\n \"as\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"items\": {\n \"type\": \"array\",\n \"items\": {},\n \"description\": \"Results from each iteration\"\n },\n \"count\": {\n \"type\": \"number\",\n \"description\": \"Number of completed iterations\"\n },\n \"totalIterations\": {\n \"type\": \"number\",\n \"description\": \"Total iterations\"\n }\n },\n \"required\": [\n \"items\",\n \"count\",\n \"totalIterations\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `control.parallel_map` — Parallel Map\n\nIterate over an array with concurrent execution up to a limit\n\n**Behavior and examples:** `eigenpal docs read steps/control/parallel_map`\n\n**Durable retry:** The control container and leaves inside its concurrent branches do not retry durably.\n\n**Config** (at step level):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `items` | string | yes | | Expression resolving to array |\n| `as` | string | yes | | Variable name for current item |\n| `indexAs` | string | no | | Variable name for current index |\n| `concurrency` | integer | no | `5` | Maximum concurrent executions (1-50, default 5) |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `items` | array<unknown> | yes | | Results from each iteration (maintains original order) |\n| `count` | number | yes | | Number of completed iterations |\n| `totalIterations` | number | yes | | Total iterations |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Expression resolving to array\"\n },\n \"as\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Variable name for current item\"\n },\n \"indexAs\": {\n \"description\": \"Variable name for current index\",\n \"type\": \"string\"\n },\n \"concurrency\": {\n \"description\": \"Maximum concurrent executions (1-50, default 5)\",\n \"default\": 5,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 50\n }\n },\n \"required\": [\n \"items\",\n \"as\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"items\": {\n \"type\": \"array\",\n \"items\": {},\n \"description\": \"Results from each iteration (maintains original order)\"\n },\n \"count\": {\n \"type\": \"number\",\n \"description\": \"Number of completed iterations\"\n },\n \"totalIterations\": {\n \"type\": \"number\",\n \"description\": \"Total iterations\"\n }\n },\n \"required\": [\n \"items\",\n \"count\",\n \"totalIterations\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `control.parallel` — Parallel\n\nExecute multiple branches concurrently\n\n**Behavior and examples:** `eigenpal docs read steps/control/parallel`\n\n**Durable retry:** The control container and leaves inside its concurrent branches do not retry durably.\n\n**Config** (at step level):\n\n_No fields._\n\n**Output:** `record<string, unknown>`\n\n> Results keyed by branch name\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {},\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {},\n \"description\": \"Results keyed by branch name\"\n}\n```\n\n\n#### `control.wait` — Wait\n\nPause workflow execution for a specified duration\n\n**Behavior and examples:** `eigenpal docs read steps/control/wait`\n\n**Durable retry:** This control step is not retried durably.\n\n**Config** (at step level):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `duration` | number | yes | | Duration in milliseconds |\n\n**Output:** `object`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `waited` | number | yes | | Actual milliseconds waited |\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"duration\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0,\n \"description\": \"Duration in milliseconds\"\n }\n },\n \"required\": [\n \"duration\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"waited\": {\n \"type\": \"number\",\n \"description\": \"Actual milliseconds waited\"\n }\n },\n \"required\": [\n \"waited\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\n#### `control.human_review` — Human Review\n\nPause the run for selective field confirmation or correction, then continue with the reviewed data.\n\n**Behavior and examples:** `eigenpal docs read steps/control/human_review`\n\n**Durable retry:** This control step is not retried durably.\n\n**Config** (in `step.with`):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `data` | string \\| record<string, unknown> \\| array<unknown> | yes | | Structured data or a template expression resolving to an object or array |\n| `schema` | record<string, unknown> | no | | Optional JSON Schema for validating edits and exposing downstream autocomplete. When omitted with metadataFrom: ai.extract and a direct whole-output data expression, the extract step schema is reused. |\n| `metadataFrom` | `\"ai.extract\"` | no | | Producer adapter. `ai.extract` derives field confidence from `_grounding` and strips that reserved key from review data. Grounding is not stripped unless this is set. |\n| `fieldMetadata` | record<string, object> | no | `{}` | RFC 6901 pointer map of producer facts (labels, confidence, display). Selection policy belongs under selection.fields; fieldMetadata.review is a legacy alias. |\n| `selection` | object | yes | | Field selection policy resolved once when the task is created |\n| `files` | object | no | `{\"includeRunInputs\":true,\"attachments\":[]}` | Run input files and optional current-run attachments for reviewers |\n| `instructions` | string | no | | Free-form guidance shown above the review workspace |\n\n**Output:** `unknown`\n\n> The complete reviewed data in the same object or array shape as the input.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"data\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"minLength\": 1\n },\n {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n {\n \"type\": \"array\",\n \"items\": {}\n }\n ],\n \"description\": \"Structured data or a template expression resolving to an object or array\"\n },\n \"schema\": {\n \"description\": \"Optional JSON Schema for validating edits and exposing downstream autocomplete. When omitted with metadataFrom: ai.extract and a direct whole-output data expression, the extract step schema is reused.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"metadataFrom\": {\n \"type\": \"string\",\n \"enum\": [\n \"ai.extract\"\n ],\n \"description\": \"Producer adapter. `ai.extract` derives field confidence from `_grounding` and strips that reserved key from review data. Grounding is not stripped unless this is set.\"\n },\n \"fieldMetadata\": {\n \"description\": \"RFC 6901 pointer map of producer facts (labels, confidence, display). Selection policy belongs under selection.fields; fieldMetadata.review is a legacy alias.\",\n \"default\": {},\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"confidence\": {\n \"anyOf\": [\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n {\n \"type\": \"string\",\n \"minLength\": 1\n }\n ]\n },\n \"label\": {\n \"type\": \"string\",\n \"maxLength\": 500\n },\n \"description\": {\n \"type\": \"string\",\n \"maxLength\": 5000\n },\n \"review\": {\n \"default\": \"auto\",\n \"type\": \"string\",\n \"enum\": [\n \"auto\",\n \"always\",\n \"never\"\n ]\n },\n \"display\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"additionalProperties\": false\n }\n },\n \"selection\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"include\": {\n \"description\": \"RFC 6901 pointers or wildcard patterns (for example `/items/*/amount`)\",\n \"default\": [],\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"maxLength\": 512\n }\n },\n \"exclude\": {\n \"description\": \"RFC 6901 pointers removed after include expansion\",\n \"default\": [],\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"maxLength\": 512\n }\n },\n \"fields\": {\n \"description\": \"Per-field selection policy keyed by exact pointers or wildcard patterns. Exact beats the most specific wildcard; equally specific overlapping wildcards are rejected.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\",\n \"maxLength\": 512\n },\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"review\": {\n \"type\": \"string\",\n \"enum\": [\n \"always\",\n \"never\",\n \"skip\"\n ]\n },\n \"threshold\": {\n \"anyOf\": [\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n {\n \"type\": \"string\",\n \"minLength\": 1\n }\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"confidence\",\n \"description\": \"Review fields below threshold or missing confidence\"\n },\n \"threshold\": {\n \"anyOf\": [\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n {\n \"type\": \"string\",\n \"minLength\": 1\n }\n ],\n \"description\": \"Fields with confidence strictly below this value require review. Equality auto-approves. Numeric 0–1 or categorical low|medium|high. Template strings are resolved at runtime.\"\n },\n \"missingConfidence\": {\n \"description\": \"Whether unscored or type-mismatched fields require review (default review)\",\n \"default\": \"review\",\n \"type\": \"string\",\n \"enum\": [\n \"review\",\n \"skip\"\n ]\n }\n },\n \"required\": [\n \"mode\",\n \"threshold\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"include\": {\n \"description\": \"RFC 6901 pointers or wildcard patterns (for example `/items/*/amount`)\",\n \"default\": [],\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"maxLength\": 512\n }\n },\n \"exclude\": {\n \"description\": \"RFC 6901 pointers removed after include expansion\",\n \"default\": [],\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"maxLength\": 512\n }\n },\n \"fields\": {\n \"description\": \"Per-field selection policy keyed by exact pointers or wildcard patterns. Exact beats the most specific wildcard; equally specific overlapping wildcards are rejected.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\",\n \"maxLength\": 512\n },\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"review\": {\n \"type\": \"string\",\n \"enum\": [\n \"always\",\n \"never\",\n \"skip\"\n ]\n },\n \"threshold\": {\n \"anyOf\": [\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n {\n \"type\": \"string\",\n \"minLength\": 1\n }\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"explicit\",\n \"description\": \"Review only paths matched by include/exclude\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"include\": {\n \"description\": \"RFC 6901 pointers or wildcard patterns (for example `/items/*/amount`)\",\n \"default\": [],\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"maxLength\": 512\n }\n },\n \"exclude\": {\n \"description\": \"RFC 6901 pointers removed after include expansion\",\n \"default\": [],\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"maxLength\": 512\n }\n },\n \"fields\": {\n \"description\": \"Per-field selection policy keyed by exact pointers or wildcard patterns. Exact beats the most specific wildcard; equally specific overlapping wildcards are rejected.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\",\n \"maxLength\": 512\n },\n \"additionalProperties\": {\n \"type\": \"object\",\n \"properties\": {\n \"review\": {\n \"type\": \"string\",\n \"enum\": [\n \"always\",\n \"never\",\n \"skip\"\n ]\n },\n \"threshold\": {\n \"anyOf\": [\n {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 1\n },\n {\n \"type\": \"string\",\n \"enum\": [\n \"low\",\n \"medium\",\n \"high\"\n ]\n },\n {\n \"type\": \"string\",\n \"minLength\": 1\n }\n ]\n }\n },\n \"additionalProperties\": false\n }\n },\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"all\",\n \"description\": \"Review every scalar leaf unless excluded or review: never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"description\": \"Field selection policy resolved once when the task is created\"\n },\n \"files\": {\n \"description\": \"Run input files and optional current-run attachments for reviewers\",\n \"default\": {\n \"includeRunInputs\": true,\n \"attachments\": []\n },\n \"type\": \"object\",\n \"properties\": {\n \"includeRunInputs\": {\n \"description\": \"Attach every authorized run input file to the review task\",\n \"default\": true,\n \"type\": \"boolean\"\n },\n \"attachments\": {\n \"description\": \"Additional current-run file or artifact template expressions\",\n \"default\": [],\n \"maxItems\": 100,\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n }\n },\n \"additionalProperties\": false\n },\n \"instructions\": {\n \"description\": \"Free-form guidance shown above the review workspace\",\n \"type\": \"string\",\n \"maxLength\": 10000\n }\n },\n \"required\": [\n \"data\",\n \"selection\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"description\": \"The complete reviewed data in the same object or array shape as the input.\"\n}\n```\n\n\n#### `control.fail` — Fail\n\nTerminate the workflow with a typed status code + message. With an optional condition, only fails when the condition is truthy; otherwise always fails when reached. Pair with ai.classify or any prior step to fail fast on bad inputs.\n\n**Behavior and examples:** `eigenpal docs read steps/control/fail`\n\n**Durable retry:** This control step is not retried durably.\n\n**Config** (at step level):\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `condition` | string | no | | Optional LiquidJS expression. When set, the step only fails if this evaluates truthy; when omitted, it always fails when reached (compose with control.if for legacy gating). |\n| `statusCode` | integer | no | `422` | HTTP-style status code returned to the caller (sync runs) and persisted on the execution. Default 422 (Unprocessable Entity). |\n| `message` | string | yes | | Human-readable failure message. Supports template expressions, e.g. \"Document classified as {{ steps.classify.output.label }}\". |\n\n**Output:** `unknown`\n\n> control.fail never produces output — it terminates the workflow when triggered.\n\n##### Complete machine-readable schemas\n\nConfig schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"condition\": {\n \"description\": \"Optional LiquidJS expression. When set, the step only fails if this evaluates truthy; when omitted, it always fails when reached (compose with control.if for legacy gating).\",\n \"type\": \"string\"\n },\n \"statusCode\": {\n \"default\": 422,\n \"description\": \"HTTP-style status code returned to the caller (sync runs) and persisted on the execution. Default 422 (Unprocessable Entity).\",\n \"type\": \"integer\",\n \"minimum\": 400,\n \"maximum\": 599\n },\n \"message\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Human-readable failure message. Supports template expressions, e.g. \\\"Document classified as {{ steps.classify.output.label }}\\\".\"\n }\n },\n \"required\": [\n \"statusCode\",\n \"message\"\n ],\n \"additionalProperties\": false\n}\n```\n\n\nOutput schema:\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"not\": {},\n \"description\": \"control.fail never produces output — it terminates the workflow when triggered.\"\n}\n```\n<!-- GENERATED:STEP_CATALOG END -->\n"
|
|
423
423
|
},
|
|
424
424
|
{
|
|
425
425
|
"topic": "reference/workflow-yaml",
|
|
426
426
|
"title": "workflow.yaml — required schema",
|
|
427
427
|
"description": "Detailed agent reference bundled with the Eigenpal CLI.",
|
|
428
428
|
"source": "agent-reference",
|
|
429
|
-
"content": "# workflow.yaml — required schema\n\nA workflow is a DAG of named steps with named inputs, named outputs, and\ntrigger metadata. The full Zod schema lives in\n`@eigenpal/types/src/workflow/` and ships with the CLI — query the\nauthoritative shape per step type with:\n\n```bash\neigenpal workflow step-type list # all step types\neigenpal workflow step-type get <type> # JSON Schema for one type's config + output\n```\n\n## Top-level required fields\n\n```yaml\nname: extract-invoices # string, lowercase kebab/snake-case, unique within tenant\nversion: 1.0.0 # bare semver `MAJOR.MINOR.PATCH`, no leading `v`\ndescription: Parse a PDF and extract invoice fields. # string, optional\ntriggerMethods: [{ type: api }] # array, ≥1 entry with type: api | manual | email\ninputs: [...] # array of input definitions (see below)\nsteps: [...] # array of steps, executed in topological order\noutput: {...} # object — the workflow's return shape\n```\n\n`name` + `version` together identify the version row. Pushing a new\nversion with the same `name` and a different `version` appends to history;\nre-using the same `version` is rejected.\n\nFolder placement is not part of `workflow.yaml`; it lives in the server\ndatabase. Use `workflow move <workflow-id> --folder <path>` to move a\nworkflow between dashboard folders. Missing folders in the path are\ncreated automatically. Use `--folder /` for root. New workflows should stay\nat the top level unless the user specifically asks to place them in a\nfolder.\n\n\n### `triggerMethods`\n\nRequired. Each entry is an object that adds an invocation surface:\n\n- `{ type: api }` — the workflow can be called via REST or the CLI's `run`\n- `{ type: manual }` — the workflow can be started manually\n- `{ type: email, whitelist?: ... }` — inbound email\n\nIf `{ type: api }` is missing, `eigenpal run workflows.<slug>` rejects with\n`403` / issue code `api_trigger_disabled` — enable the API trigger in the\nworkflow editor or dashboard settings. Runtime gates read the\n`automation_triggers` projection (synced on workflow publish).\n\n### `inputs`\n\n```yaml\ninputs:\n - name: document # required — becomes a key on triggerInput\n type: file # one of: string | number | boolean | object | file\n file: true # required when type=file (legacy duplicate of `type: file`)\n description: PDF or image to extract from\n required: true # default true; pass false for optional inputs\n\n - name: language\n type: string\n description: Language hint\n default: auto\n```\n\nPer input:\n\n- `name` — `[a-z0-9][a-z0-9-_]*`\n- `type` — `string` | `number` | `boolean` | `object` | `array` | `file`\n- `file: true` for binary inputs (PDF, DOCX, image). The CLI uploads,\n then references via `{{ input.<name> }}` which resolves to a\n `FilePathDescriptor` the worker reads.\n- `default` — used when the input is omitted at runtime\n- `description` — surfaced in the dashboard + agent tools\n\n#### File input shape — single vs array\n\nThe materialized shape on `triggerInput` is driven by the YAML, not by\nhow many files the dataset uploaded. Get this wrong and the worker\nsees the opposite shape:\n\n| YAML | durable `triggerInput.<name>` | Template |\n| ------------------------------------------ | ------------------------------ | --------------------------------------- |\n| `type: file` | `{ \"$file\": \"input/...\" }` | `{{ input.<name> }}` |\n| `type: array`, `items: { type: file }` | `[{ \"$file\": \"input/...\" }]` | `{{ input.<name>[0] }}` (or `forEach:`) |\n\nAPI/SDK ingress can use `{ \"$fileId\": \"file_...\" }`, inline bytes via\n`{ \"$inline\": { filename, mimeType, base64 } }`, or multipart file parts.\nEigenpal snapshots those into run-scoped artifacts before execution.\n\nIf you see `Invalid input ... expected object, received array` from a\nstep that takes a single file, the workflow input is `type: file` but\nthe dataset folder probably has multiple files in it (or the workflow\nwas on a stale version that materialized arrays unconditionally).\nTrim the folder to one file, or switch the input to the array form.\n\n### `steps`\n\n```yaml\nsteps:\n - name: parse # required, unique within the workflow\n type: ai.parse # see `reference/step-types.md`; introspect with `step-type get`\n if: '{{ input.skipParse != true }}' # optional — Liquid expression; falsy skips the step\n forEach: '{{ input.documents }}' # optional — runs the step over each item; output is array\n with: # step-specific config — schema depends on `type`\n file: '{{ input.document }}'\n parser: auto\n\n - name: extract\n type: ai.extract\n with:\n input: '{{ steps.parse.output.text }}'\n schema:\n type: object\n properties:\n invoiceNumber: { type: string }\n totalAmount: { type: number }\n required: [invoiceNumber]\n```\n\nStep references resolve via Liquid template expressions:\n\n- `{{ input.<name> }}` — workflow input\n- `{{ steps.<step-name>.output.<field> }}` — prior step output\n- `{{ item.<field> }}` — current item inside `forEach`\n\n`steps[].name` is the only handle into the step from later steps. Use\nshort, descriptive names (`parse`, `extract`, `score`).\n\n<!-- GENERATED:RETRY_REFERENCE START -->\n## Durable retry policies\n\n_Policy syntax is generated from `WorkflowRetryPolicySchema` and `StepRetryPolicySchema`; step capability notes are generated from `STEP_RETRY_CAPABILITIES` in `@eigenpal/types`._\n\n- Workflow policy values: `automatic`, `never` or an object with `mode` and `maxAttempts`.\n- Step policy values: `inherit`, `automatic`, `never`. `automatic` supports object form with `mode` and `maxAttempts`; `never` supports `{ mode: 'never' }`; `inherit` is string-only.\n- The schema accepts `maxAttempts` from 1 through 10. Studio offers 2-3 total attempts, and the current worker ceiling is 3.\n- `maxAttempts` includes the first attempt. If no policy is set, durable retries are off.\n\n```yaml\nsettings:\n retry:\n mode: automatic\n maxAttempts: 3\n\nsteps:\n - name: fetch-catalog\n type: action.http\n retry: inherit\n with:\n method: GET\n url: 'https://api.example.com/catalog'\n\n - name: read-product-page\n type: action.website-reader\n retry: never\n with:\n url: '{{ input.url }}'\n```\n\n`automatic` retries transient timeouts, rate limits, and selected retryable server failures. Delays use bounded exponential backoff, honor `Retry-After`, and stop after a five-minute elapsed budget.\n\nDurable leaf retries are supported for Website Reader and HTTP `GET`/`HEAD`. Unsafe HTTP methods, Invoke Workflow, AI steps (which may have separate provider request retries), and transforms or file outputs are not durably replayed. Control containers are not attempts themselves; eligible leaves inside sequential If, Switch, and For Each scopes may retry, while concurrent Parallel and Parallel Map branches do not. See each generated step entry for its capability.\n\nLegacy whole-run retry counts are accepted for compatibility but no longer restart failed runs. Move retry intent to the workflow retry default or an eligible step.\n<!-- GENERATED:RETRY_REFERENCE END -->\n\n### `output`\n\n```yaml\noutput:\n invoiceNumber: '{{ steps.extract.output.invoiceNumber }}'\n totalAmount: '{{ steps.extract.output.totalAmount }}'\n confidence: '{{ steps.score.output.confidence | default: 0 }}'\n```\n\nWhatever shape you write here is the workflow's return value. Reference\nany step's output. Static keys are fine — only the values are templated.\n\n## Step types — pick the right one\n\nSee `reference/step-types.md` for the full catalog. Categories:\n\n- `action.*` — side-effectful operations (HTTP, email, connectors)\n- `ai.*` — model-backed processing (parse, extract, classify, judge)\n- `transform.*` — deterministic data transforms (script, template, merge)\n- `control.*` — flow control (if, for-each, parallel-map, fail)\n\nWhen in doubt, use `eigenpal workflow step-type get <type>` — it returns\nthe exact JSON Schema for the step's `config` + `output` so you know\nwhich fields are required and what values they accept.\n\n## Liquid vs `transform.script`\n\nUse Liquid `{{ ... }}` for one-liners:\n\n| Use case | Use |\n| ------------- | -------------------------------- |\n| Field access | `{{ steps.x.output.field }}` |\n| Default value | `{{ value \\| default: 0 }}` |\n| Simple math | `{{ price \\| times: quantity }}` |\n\nAnything more complex (array reduce/filter, multi-step calc, date\narithmetic, regex) goes in `transform.script`. The function is\nTypeScript; the return-type annotation IS this step's output schema\n(there is no separate `outputSchema:` field):\n\n```yaml\n- name: total\n type: transform.script\n with:\n inputs: # explicit data deps — visible to the validator\n lineItems: '{{ steps.extract.output.lineItems }}'\n taxRate: '{{ input.taxRate }}'\n function: |\n function script(\n lineItems: { price: number; qty: number }[],\n taxRate: number,\n ): { subtotal: number; tax: number; total: number } {\n const subtotal = lineItems.reduce((s, i) => s + i.price * i.qty, 0);\n const tax = subtotal * taxRate;\n return { subtotal, tax, total: subtotal + tax };\n }\n```\n\nThe `): { subtotal: number; tax: number; total: number }` annotation is\nrequired and IS this step's output schema. Downstream steps autocomplete\nagainst it and reference its fields via `{{ steps.<this>.output.<field> }}`,\nand the worker validates the actual return value against it. Describe what\nthe function really returns: a too-loose annotation like `unknown` produces\nan empty schema, which makes downstream `output.<field>` references\nunresolvable and trips \"field not found\" warnings.\n\nThe script runs in a WASM sandbox: 5s wall clock, 10 MB heap, no network,\nno filesystem. Trips show up as `script_timeout` / `script_memory_limit`\non the step execution.\n\n## Be specific with types\n\nEvery output schema you write is read three times: by the LLM (for\n`ai.extract`), by downstream steps (autocomplete + template resolution),\nand by the runtime validator. **The more specific the type, the better\nthe workflow runs.**\n\n### Use `enum` for coded values\n\nCategorical fields like `category`, `status`, `kind`, `severity`,\n`monitoring`, `frequency` should be enums whenever the value set is\nclosed. The LLM is constrained to emit only allowed values, and\ndownstream `transform.script` steps know exactly what they'll see.\n\n```yaml\n- name: classify\n type: ai.extract\n with:\n input: '{{ input.text }}'\n schema:\n type: object\n properties:\n category:\n type: string\n enum: ['901', '902']\n description: |\n Covenant category.\n 901 = Odkladacie podmienky (conditions precedent).\n 902 = Následné podmienky (subsequent / ongoing covenants).\n required: [category]\n```\n\nPer-value meanings go inline in the field's `description`. The LLM picks\nthem up reliably from there, and the scope browser renders the allowed\nvalues when downstream steps reference the field.\n\n### `transform.script` — TS literal unions become JSON Schema enums\n\nA TS literal-union return type compiles to a JSON-Schema enum\nautomatically. Use it when the function returns a value from a closed set:\n\n```yaml\n- name: severity\n type: transform.script\n with:\n inputs:\n score: '{{ steps.score.output.value }}'\n function: |\n function script(score: number): { level: 'low' | 'medium' | 'high' } {\n return { level: score > 0.8 ? 'high' : score > 0.4 ? 'medium' : 'low' };\n }\n```\n\nDownstream steps autocomplete `low | medium | high` for `level`.\n\n### `eigenpal workflow push` warns on weak types\n\nPush surfaces non-fatal **schema-quality warnings** when it sees patterns\nthat hurt downstream typing. Common ones:\n\n| Warning code | What it means |\n| ---------------------------- | -------------------------------------------------------------------------------------------- |\n| `categorical-missing-enum` | A field named like `category` / `status` / `kind` is a plain `string`. Consider declaring an `enum`. |\n| `weak-script-return` | A `transform.script` returns `any` or `unknown`. Replace with a concrete shape or a literal union. |\n| `untyped-object` | A `type: object` field has no `properties`. The LLM has no guidance on what to emit. |\n| `untyped-array-items` | A `type: array` field has no `items:` declared. |\n| `unknown-step-reference` | A Liquid expression references a step that is not in scope, often because it is inside a control container. |\n\nWarnings do not block the push. They print to stderr as `! …` lines. Run\n`eigenpal workflow validate` locally to surface them before pushing.\n\n## Common patterns\n\n### Conditional branches\n\n```yaml\n- name: classify\n type: ai.extract\n with:\n input: '{{ input.text }}'\n schema:\n type: object\n properties:\n kind: { type: string, enum: [invoice, contract, other] }\n\n- name: extract-invoice\n type: ai.extract\n if: '{{ steps.classify.output.kind == \"invoice\" }}'\n with: { ... }\n\n- name: extract-contract\n type: ai.extract\n if: '{{ steps.classify.output.kind == \"contract\" }}'\n with: { ... }\n```\n\n### Map over an array input\n\n```yaml\n- name: extract-each\n type: ai.extract\n forEach: '{{ input.documents }}'\n with:\n input: '{{ item.text }}'\n schema: { ... }\n# steps.extract-each.output is an array of {invoiceNumber, totalAmount, ...}\n```\n\n### Parallel fan-out\n\n```yaml\n- name: extract-fields\n type: control.parallel-map\n forEach: '{{ input.fieldNames }}'\n with:\n steps:\n - name: extract-one\n type: ai.extract\n with:\n input: '{{ input.document }}'\n schema: { type: object, properties: { value: { type: string } } }\n```\n\n### Split a long doc into named sections, then extract each\n\nFor documents that bundle multiple sub-documents (e.g. a contract pack with\nseveral annexes), use `ai.split` to find section boundaries with an LLM,\nthen `control.parallel_map` to run section-specific `ai.extract`. The\nsplitter operates on parsed pages, so DOCX / RTF / scans all reduce to the\nsame per-page representation.\n\nDescription-writing tips (mirrors Reducto's guidance):\n\n- Use the document's own terminology — a Slovak contract calls it `Príloha`,\n not \"annex\". List the multilingual variants you have seen.\n- Mention stable visual cues if any — a centered title-page header, a\n signature line, the start of a specific table.\n- Be specific. \"The collateral schedule table at the start of Príloha 2\"\n beats \"annex\".\n\n```yaml\n- name: parse\n type: ai.parse\n with:\n input: '{{ input.document }}'\n\n- name: split\n type: ai.split\n with:\n input: '{{ steps.parse.output }}'\n sections:\n - name: intro\n description: >-\n The introductory cover/preamble of the loan pack — borrower details,\n loan purpose, signature block. Always before any \"Príloha\".\n - name: priloha_2\n description: >-\n \"Príloha č. 2 / Anlage 2 / Załącznik nr 2\" — the collateral\n schedule. Starts with a section header on its own page; contains\n a table of pledged assets.\n required: true # warn if not found\n endHints: # LLM-judged end cues — phrases the model treats semantically\n - \"PRÍLOHA Č. 3\"\n - \"ANLAGE 3\"\n - name: priloha_3\n description: >-\n \"Príloha č. 3 / Anlage 3\" — the repayment schedule. Tabular\n amortization plan with monthly rows.\n rules: |\n End-of-section markers like *Koniec prílohy 2* close the current section.\n Repeated headers/footers are continuation, not new sections.\n\n- name: extract-each-section\n type: control.parallel_map\n items: '{{ steps.split.output.splits }}'\n as: section\n steps:\n - name: extract-section-fields\n type: ai.extract\n with:\n input: '{{ section.text }}'\n schema:\n type: object\n properties:\n section_name: { type: string }\n key_facts: { type: array, items: { type: string } }\n```\n\nEach `splits[i]` carries `{ name, page_range, confidence, notes, evidence,\nend_evidence, text, pages, pages_anchored, pages_inferred }`.\n\n- `confidence` is `'low' | 'medium' | 'high'` — coarse enum, not a 0..1\n number. LLMs cluster numeric scores meaninglessly; the enum is reliable.\n- `evidence.start_heading_text` is the verbatim heading the LLM cited (parse\n this instead of regexing over `notes`).\n- `end_evidence` is set when the LLM detected a section close (matched\n against the section's `endHints` or an explicit closing marker like\n `*Koniec prílohy*`). Carries `{ end_page, confidence, notes }`. Absent\n when continuity-fill alone closed the section. Pair `evidence.notes` +\n `end_evidence.notes` for a reconciliation pass that judges whether the\n section is correctly bounded.\n- `pages[i].source` is `'anchored'` (direct LLM evidence) or `'inferred'`\n (filled by continuity) — use it to deprioritize inferred content.\n\nPages that match no section are silently dropped (Reducto-style); add an\nexplicit \"everything else\" section if you need a catch-all.\n\nPer-section knobs:\n\n- `required: true` — log a warn if missing and lean the LLM toward expecting\n it. Default: false.\n- `endHints: [string]` — natural-language cues the LLM uses to find the\n section's END. Treated semantically (casing variants, missing diacritics,\n multilingual phrasings all qualify) — this is NOT a regex match. The LLM\n emits a structured `end_anchor`; deterministic merge applies it. Replaces\n the older `endAnchors` line-prefix matcher.\n\n`extract-each-section.output` is `{ items, count, totalIterations }`, not a\nflat array — `items[i]` holds the per-iteration result keyed by the last\ninner step's name (here `{ \"extract-section-fields\": {...} }`).\n\n## Validate before pushing\n\n```bash\neigenpal workflow validate ./workflow.yaml\n```\n\nReports field-level issues:\n\n```\n✗ workflow (./workflow.yaml) — 2 issues\n steps.2.config.passThreshold Required\n steps.2.type Invalid step type: 'eval.llm-judg'\n```\n\nPush:\n\n```bash\n# Create a new workflow (version comes from the YAML's top-level `version:` field)\neigenpal workflow push --file workflow.yaml\n\n# Update an existing one — three ways to set the new version:\neigenpal workflow push --file workflow.yaml --workflow-id wf_… # bump in YAML's `version:`\neigenpal workflow push --file workflow.yaml --workflow-id wf_… --set-version 2.0.0 # explicit semver\neigenpal workflow push --file workflow.yaml --workflow-id wf_… --bump patch # auto-bump (patch | minor | major)\n```\n\n`--bump` requires `--workflow-id` (it bumps from the current server\nversion). `--set-version` works with or without `--workflow-id`. The\nYAML's top-level `version:` field is the fallback when neither flag is\npassed — server rejects pushes whose version does not strictly increase.\n\n## Discover step + evaluator types\n\nDo not memorize fields — introspect:\n\n```bash\neigenpal workflow step-type list # full step catalog\neigenpal workflow step-type get <type> # config + output schema for one step type\neigenpal workflow evaluator-type list # full evaluator catalog\neigenpal workflow evaluator-type get <type> # config schema for one evaluator type\n```\n\nBoth catalogs are generated from the canonical Zod schemas in\n`@eigenpal/types`, so they are always in sync with what the deployment\nsupports.\n\n<!-- GENERATED:WORKFLOW_REFERENCE START -->\n## Top-level fields — full reference\n\n_Generated from `WorkflowDefinitionSchema` in `@eigenpal/types/src/workflow/workflow.ts`. Do not hand-edit between the GENERATED fences — run `bun run --cwd packages/cli generate:skill`._\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `name` | string | yes | | URL-safe workflow slug: 1-64 lowercase letters, digits, underscores, or hyphens; must start with a letter or digit. |\n| `version` | string | no | `\"1.0.0\"` | Author-provided semantic version label stored with the definition. |\n| `description` | string | no | | Human-readable purpose of the workflow. |\n| `enabled` | boolean | no | `true` | Whether the workflow may be run. |\n| `triggerMethods` | array<object> | no | `[{\"type\":\"manual\"}]` | Ways this workflow can be invoked: manual, api, or email. |\n| `inputs` | array<object> | no | | Top-level inputs available under `input` in template expressions. |\n| `steps` | array<unknown> | yes | | Ordered workflow steps; names must be unique. |\n| `output` | record<string, string> \\| string | no | | Final output as named field-to-template mappings or one passthrough template expression. |\n| `settings` | object | no | | Workflow-wide timeout and retry defaults. |\n| `defaultModel` | string | no | | Default configured LLM provider id used by AI steps when the step does not select one. |\n\n\n## Per-input fields\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `name` | string | yes | | Top-level input name used as `input.<name>` in template expressions. |\n| `type` | string | yes | | One of string, enum, number, integer, boolean, array, object, or file. |\n| `description` | string | no | | Human-readable input meaning shown to callers. |\n| `required` | boolean | no | `true` | Whether callers must provide this input. |\n| `default` | unknown | no | | Value used when an optional input is omitted. |\n| `values` | array<string> | no | | Closed set of allowed strings when type is enum. |\n| `items` | object | no | | Element definition when type is array. |\n| `properties` | array<unknown> | no | | Recursive field definitions when type is object. |\n| `source` | string | no | | Registered external file resolver for single-tenant string-id file inputs, for example gpfs; valid only with type file. |\n| `mimeType` | string | no | | MIME hint such as application/pdf for a sourced file; mutually exclusive with extension. |\n| `extension` | string | no | | Extension hint such as pdf for a sourced file; mutually exclusive with mimeType. |\n\n\n## Nested input property fields\n\nObject inputs and object array items use this recursive shape. File inputs are top-level only.\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `name` | string | yes | | JSON object key, addressable in templates with dot notation. |\n| `type` | string | yes | | One of string, enum, number, integer, boolean, array, or object. |\n| `description` | string | no | | Human-readable field meaning. |\n| `required` | boolean | no | | Whether this property must be present; defaults to true when omitted. |\n| `values` | array<string> | no | | Allowed strings when type is enum. |\n| `items` | object | no | | Element definition when type is array. |\n| `properties` | array<unknown> | no | | Recursive fields when type is object. |\n\n\n## Trigger method variants\n\n### `manual`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `type` | `\"manual\"` | yes | | Dashboard/manual run trigger. |\n| `enabled` | boolean | no | | Set false to disable dashboard runs; manual triggering is enabled by default. |\n| `inputSchema` | record<string, unknown> | no | | Optional JSON Schema for trigger input. |\n\n\n### `api`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `type` | `\"api\"` | yes | | Public API run trigger. |\n| `inputSchema` | record<string, unknown> | no | | Optional JSON Schema for API input. |\n\n\n### `email`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `type` | `\"email\"` | yes | | Inbound email trigger. |\n| `whitelist` | object | no | | Optional sender allowlist by exact email address or domain. |\n\n\n## Workflow settings\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `timeout` | number | no | | Default step timeout in milliseconds. |\n| `retry` | `\"automatic\"` \\| `\"never\"` \\| object | no | | Default durable retry policy for steps. |\n| `retries` | integer | no | | |\n| `retryDelay` | number | no | | |\n\n\n## Complete machine-readable workflow schema\n\nThis JSON Schema is generated from the same Zod schema used to parse workflow YAML. YAML keys and nesting are identical.\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 64,\n \"pattern\": \"^[a-z0-9][a-z0-9_-]*$\",\n \"description\": \"URL-safe workflow slug: 1-64 lowercase letters, digits, underscores, or hyphens; must start with a letter or digit.\"\n },\n \"kind\": {\n \"description\": \"Deprecated compatibility discriminator; omit for new workflows.\",\n \"default\": \"workflow\",\n \"type\": \"string\",\n \"enum\": [\n \"workflow\"\n ]\n },\n \"version\": {\n \"default\": \"1.0.0\",\n \"description\": \"Author-provided semantic version label stored with the definition.\",\n \"type\": \"string\"\n },\n \"description\": {\n \"description\": \"Human-readable purpose of the workflow.\",\n \"type\": \"string\"\n },\n \"enabled\": {\n \"default\": true,\n \"description\": \"Whether the workflow may be run.\",\n \"type\": \"boolean\"\n },\n \"triggerMethods\": {\n \"default\": [\n {\n \"type\": \"manual\"\n }\n ],\n \"description\": \"Ways this workflow can be invoked: manual, api, or email.\",\n \"type\": \"array\",\n \"items\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"manual\",\n \"description\": \"Dashboard/manual run trigger.\"\n },\n \"enabled\": {\n \"description\": \"Set false to disable dashboard runs; manual triggering is enabled by default.\",\n \"type\": \"boolean\"\n },\n \"inputSchema\": {\n \"description\": \"Optional JSON Schema for trigger input.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"api\",\n \"description\": \"Public API run trigger.\"\n },\n \"inputSchema\": {\n \"description\": \"Optional JSON Schema for API input.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"email\",\n \"description\": \"Inbound email trigger.\"\n },\n \"whitelist\": {\n \"description\": \"Optional sender allowlist by exact email address or domain.\",\n \"type\": \"object\",\n \"properties\": {\n \"domains\": {\n \"description\": \"Allowed sender domains.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"emails\": {\n \"description\": \"Allowed exact sender email addresses.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"type\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"inputs\": {\n \"description\": \"Top-level inputs available under `input` in template expressions.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Top-level input name used as `input.<name>` in template expressions.\"\n },\n \"type\": {\n \"type\": \"string\",\n \"description\": \"One of string, enum, number, integer, boolean, array, object, or file.\"\n },\n \"description\": {\n \"description\": \"Human-readable input meaning shown to callers.\",\n \"type\": \"string\"\n },\n \"required\": {\n \"default\": true,\n \"description\": \"Whether callers must provide this input.\",\n \"type\": \"boolean\"\n },\n \"default\": {\n \"description\": \"Value used when an optional input is omitted.\"\n },\n \"values\": {\n \"description\": \"Closed set of allowed strings when type is enum.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"items\": {\n \"description\": \"Element definition when type is array.\",\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"description\": \"Array element type: string, enum, number, integer, boolean, object, or file.\"\n },\n \"values\": {\n \"description\": \"Closed set of allowed strings when items.type is enum.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"properties\": {\n \"description\": \"Recursive fields when items.type is object.\",\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema0\"\n }\n }\n },\n \"required\": [\n \"type\"\n ],\n \"additionalProperties\": false\n },\n \"properties\": {\n \"description\": \"Recursive field definitions when type is object.\",\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema0\"\n }\n },\n \"source\": {\n \"description\": \"Registered external file resolver for single-tenant string-id file inputs, for example gpfs; valid only with type file.\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"mimeType\": {\n \"description\": \"MIME hint such as application/pdf for a sourced file; mutually exclusive with extension.\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"extension\": {\n \"description\": \"Extension hint such as pdf for a sourced file; mutually exclusive with mimeType.\",\n \"type\": \"string\",\n \"minLength\": 1\n }\n },\n \"required\": [\n \"name\",\n \"type\",\n \"required\"\n ],\n \"additionalProperties\": false\n }\n },\n \"steps\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n },\n \"description\": \"Ordered workflow steps; names must be unique.\"\n },\n \"output\": {\n \"description\": \"Final output as named field-to-template mappings or one passthrough template expression.\",\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"string\"\n }\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"settings\": {\n \"description\": \"Workflow-wide timeout and retry defaults.\",\n \"type\": \"object\",\n \"properties\": {\n \"timeout\": {\n \"description\": \"Default step timeout in milliseconds.\",\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"description\": \"Default durable retry policy for steps.\",\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n }\n },\n \"additionalProperties\": false\n },\n \"defaultModel\": {\n \"description\": \"Default configured LLM provider id used by AI steps when the step does not select one.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\",\n \"version\",\n \"enabled\",\n \"triggerMethods\",\n \"steps\"\n ],\n \"additionalProperties\": false,\n \"definitions\": {\n \"__schema0\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"pattern\": \"^[a-zA-Z_][a-zA-Z0-9_]*$\",\n \"description\": \"JSON object key, addressable in templates with dot notation.\"\n },\n \"type\": {\n \"type\": \"string\",\n \"description\": \"One of string, enum, number, integer, boolean, array, or object.\"\n },\n \"description\": {\n \"description\": \"Human-readable field meaning.\",\n \"type\": \"string\"\n },\n \"required\": {\n \"description\": \"Whether this property must be present; defaults to true when omitted.\",\n \"type\": \"boolean\"\n },\n \"values\": {\n \"description\": \"Allowed strings when type is enum.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"items\": {\n \"description\": \"Element definition when type is array.\",\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"description\": \"Array element type: string, enum, number, integer, boolean, or object.\"\n },\n \"values\": {\n \"description\": \"Allowed strings when items.type is enum.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"properties\": {\n \"description\": \"Recursive object fields when items.type is object.\",\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema0\"\n }\n }\n },\n \"required\": [\n \"type\"\n ],\n \"additionalProperties\": false\n },\n \"properties\": {\n \"description\": \"Recursive fields when type is object.\",\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema0\"\n }\n }\n },\n \"required\": [\n \"name\",\n \"type\"\n ],\n \"additionalProperties\": false\n },\n \"__schema1\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"ai.parse\",\n \"ai.extract\",\n \"ai.split\",\n \"ai.segment\",\n \"ai.classify\",\n \"ai.classify-pages\",\n \"ai.vision\",\n \"ai.search-files\",\n \"ai.figure-crops\"\n ]\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\",\n \"name\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"transform.set\",\n \"transform.remove\",\n \"transform.combine\",\n \"transform.split\",\n \"transform.merge\",\n \"transform.template\",\n \"transform.pdf-embed\",\n \"transform.xlsx-to-json\",\n \"transform.json-to-xlsx\",\n \"transform.script\",\n \"transform.text-chunker\",\n \"transform.regex-extract\",\n \"transform.archive-list\",\n \"transform.archive-extract\"\n ]\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\",\n \"name\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"action.http\",\n \"action.invoke-workflow\",\n \"action.website-reader\",\n \"action.email\"\n ]\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\",\n \"name\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.wait\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"duration\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n }\n },\n \"required\": [\n \"type\",\n \"name\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.human_review\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"with\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.fail\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"condition\": {\n \"type\": \"string\"\n },\n \"statusCode\": {\n \"type\": \"integer\",\n \"minimum\": 400,\n \"maximum\": 599\n },\n \"message\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.block\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"blockName\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"inputs\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"blockName\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.if\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"condition\": {\n \"type\": \"string\"\n },\n \"then\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n },\n \"else\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"condition\",\n \"then\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.switch\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"on\": {\n \"type\": \"string\"\n },\n \"cases\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"when\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"boolean\"\n }\n ]\n },\n \"steps\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n }\n },\n \"required\": [\n \"when\",\n \"steps\"\n ],\n \"additionalProperties\": false\n }\n },\n \"default\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"on\",\n \"cases\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.parallel\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"branches\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"steps\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n }\n },\n \"required\": [\n \"name\",\n \"steps\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"branches\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.foreach\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"items\": {\n \"type\": \"string\"\n },\n \"as\": {\n \"type\": \"string\"\n },\n \"indexAs\": {\n \"type\": \"string\"\n },\n \"steps\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"items\",\n \"as\",\n \"steps\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.parallel_map\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"items\": {\n \"type\": \"string\"\n },\n \"as\": {\n \"type\": \"string\"\n },\n \"indexAs\": {\n \"type\": \"string\"\n },\n \"concurrency\": {\n \"default\": 5,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 50\n },\n \"steps\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"items\",\n \"as\",\n \"steps\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n }\n}\n```\n<!-- GENERATED:WORKFLOW_REFERENCE END -->\n"
|
|
429
|
+
"content": "# workflow.yaml — required schema\n\nA workflow is a DAG of named steps with named inputs, named outputs, and\ntrigger metadata. The full Zod schema lives in\n`@eigenpal/types/src/workflow/` and ships with the CLI — query the\nauthoritative shape per step type with:\n\n```bash\neigenpal workflow step-type list # all step types\neigenpal workflow step-type get <type> # JSON Schema for one type's config + output\n```\n\n## Top-level required fields\n\n```yaml\nname: extract-invoices # string, lowercase kebab/snake-case, unique within tenant\nversion: 1.0.0 # bare semver `MAJOR.MINOR.PATCH`, no leading `v`\ndescription: Parse a PDF and extract invoice fields. # string, optional\ntriggerMethods: [{ type: api }] # array, ≥1 entry with type: api | manual | email\ninputs: [...] # array of input definitions (see below)\nsteps: [...] # array of steps, executed in topological order\noutput: {...} # object — the workflow's return shape\n```\n\n`name` + `version` together identify the version row. Pushing a new\nversion with the same `name` and a different `version` appends to history;\nre-using the same `version` is rejected.\n\nFolder placement is not part of `workflow.yaml`; it lives in the server\ndatabase. Use `workflow move <workflow-id> --folder <path>` to move a\nworkflow between dashboard folders. Missing folders in the path are\ncreated automatically. Use `--folder /` for root. New workflows should stay\nat the top level unless the user specifically asks to place them in a\nfolder.\n\n\n### `triggerMethods`\n\nRequired. Each entry is an object that adds an invocation surface:\n\n- `{ type: api }` — the workflow can be called via REST or the CLI's `run`\n- `{ type: manual }` — the workflow can be started manually\n- `{ type: email, whitelist?: ... }` — inbound email\n\nIf `{ type: api }` is missing, `eigenpal run workflows.<slug>` rejects with\n`403` / issue code `api_trigger_disabled` — enable the API trigger in the\nworkflow editor or dashboard settings. Runtime gates read the\n`automation_triggers` projection (synced on workflow publish).\n\n### `inputs`\n\n```yaml\ninputs:\n - name: document # required — becomes a key on triggerInput\n type: file # one of: string | number | boolean | object | file\n file: true # required when type=file (legacy duplicate of `type: file`)\n description: PDF or image to extract from\n required: true # default true; pass false for optional inputs\n\n - name: language\n type: string\n description: Language hint\n default: auto\n```\n\nPer input:\n\n- `name` — `[a-z0-9][a-z0-9-_]*`\n- `type` — `string` | `number` | `boolean` | `object` | `array` | `file`\n- `file: true` for binary inputs (PDF, DOCX, image). The CLI uploads,\n then references via `{{ input.<name> }}` which resolves to a\n `FilePathDescriptor` the worker reads.\n- `default` — used when the input is omitted at runtime\n- `description` — surfaced in the dashboard + agent tools\n\n#### File input shape — single vs array\n\nThe materialized shape on `triggerInput` is driven by the YAML, not by\nhow many files the dataset uploaded. Get this wrong and the worker\nsees the opposite shape:\n\n| YAML | durable `triggerInput.<name>` | Template |\n| ------------------------------------------ | ------------------------------ | --------------------------------------- |\n| `type: file` | `{ \"$file\": \"input/...\" }` | `{{ input.<name> }}` |\n| `type: array`, `items: { type: file }` | `[{ \"$file\": \"input/...\" }]` | `{{ input.<name>[0] }}` (or `forEach:`) |\n\nAPI/SDK ingress can use `{ \"$fileId\": \"file_...\" }`, inline bytes via\n`{ \"$inline\": { filename, mimeType, base64 } }`, or multipart file parts.\nEigenpal snapshots those into run-scoped artifacts before execution.\n\nIf you see `Invalid input ... expected object, received array` from a\nstep that takes a single file, the workflow input is `type: file` but\nthe dataset folder probably has multiple files in it (or the workflow\nwas on a stale version that materialized arrays unconditionally).\nTrim the folder to one file, or switch the input to the array form.\n\n### `steps`\n\n```yaml\nsteps:\n - name: parse # required, unique within the workflow\n type: ai.parse # see `reference/step-types.md`; introspect with `step-type get`\n if: '{{ input.skipParse != true }}' # optional — Liquid expression; falsy skips the step\n forEach: '{{ input.documents }}' # optional — runs the step over each item; output is array\n with: # step-specific config — schema depends on `type`\n file: '{{ input.document }}'\n parser: auto\n\n - name: extract\n type: ai.extract\n with:\n input: '{{ steps.parse.output.text }}'\n schema:\n type: object\n properties:\n invoiceNumber: { type: string }\n totalAmount: { type: number }\n required: [invoiceNumber]\n```\n\nStep references resolve via Liquid template expressions:\n\n- `{{ input.<name> }}` — workflow input\n- `{{ steps.<step-name>.output.<field> }}` — prior step output\n- `{{ item.<field> }}` — current item inside `forEach`\n\n`steps[].name` is the only handle into the step from later steps. Use\nshort, descriptive names (`parse`, `extract`, `score`).\n\n<!-- GENERATED:RETRY_REFERENCE START -->\n## Durable retry policies\n\n_Policy syntax is generated from `WorkflowRetryPolicySchema` and `StepRetryPolicySchema`; step capability notes are generated from `STEP_RETRY_CAPABILITIES` in `@eigenpal/types`._\n\n- Workflow policy values: `automatic`, `never` or an object with `mode` and `maxAttempts`.\n- Step policy values: `inherit`, `automatic`, `never`. `automatic` supports object form with `mode` and `maxAttempts`; `never` supports `{ mode: 'never' }`; `inherit` is string-only.\n- The schema accepts `maxAttempts` from 1 through 10. Studio offers 2-3 total attempts, and the current worker ceiling is 3.\n- `maxAttempts` includes the first attempt. If no policy is set, durable retries are off.\n\n```yaml\nsettings:\n retry:\n mode: automatic\n maxAttempts: 3\n\nsteps:\n - name: fetch-catalog\n type: action.http\n retry: inherit\n with:\n method: GET\n url: 'https://api.example.com/catalog'\n\n - name: read-product-page\n type: action.website-reader\n retry: never\n with:\n url: '{{ input.url }}'\n```\n\n`automatic` retries transient timeouts, rate limits, and selected retryable server failures. Delays use bounded exponential backoff, honor `Retry-After`, and stop after a five-minute elapsed budget.\n\nDurable leaf retries are supported for Website Reader and HTTP `GET`/`HEAD`. Unsafe HTTP methods, Invoke Workflow, AI steps (which may have separate provider request retries), and transforms or file outputs are not durably replayed. Control containers are not attempts themselves; eligible leaves inside sequential If, Switch, and For Each scopes may retry, while concurrent Parallel and Parallel Map branches do not. See each generated step entry for its capability.\n\nLegacy whole-run retry counts are accepted for compatibility but no longer restart failed runs. Move retry intent to the workflow retry default or an eligible step.\n<!-- GENERATED:RETRY_REFERENCE END -->\n\n### `output`\n\n```yaml\noutput:\n invoiceNumber: '{{ steps.extract.output.invoiceNumber }}'\n totalAmount: '{{ steps.extract.output.totalAmount }}'\n confidence: '{{ steps.score.output.confidence | default: 0 }}'\n```\n\nWhatever shape you write here is the workflow's return value. Reference\nany step's output. Static keys are fine — only the values are templated.\n\n## Step types — pick the right one\n\nSee `reference/step-types.md` for the full catalog. Categories:\n\n- `action.*` — side-effectful operations (HTTP, email, connectors)\n- `ai.*` — model-backed processing (parse, extract, classify, judge)\n- `transform.*` — deterministic data transforms (script, template, merge)\n- `control.*` — flow control (if, for-each, parallel-map, fail)\n\nWhen in doubt, use `eigenpal workflow step-type get <type>` — it returns\nthe exact JSON Schema for the step's `config` + `output` so you know\nwhich fields are required and what values they accept.\n\n## Liquid vs `transform.script`\n\nUse Liquid `{{ ... }}` for one-liners:\n\n| Use case | Use |\n| ------------- | -------------------------------- |\n| Field access | `{{ steps.x.output.field }}` |\n| Default value | `{{ value \\| default: 0 }}` |\n| Simple math | `{{ price \\| times: quantity }}` |\n\nAnything more complex (array reduce/filter, multi-step calc, date\narithmetic, regex) goes in `transform.script`. The function is\nTypeScript; the return-type annotation IS this step's output schema\n(there is no separate `outputSchema:` field):\n\n```yaml\n- name: total\n type: transform.script\n with:\n inputs: # explicit data deps — visible to the validator\n lineItems: '{{ steps.extract.output.lineItems }}'\n taxRate: '{{ input.taxRate }}'\n function: |\n function script(\n lineItems: { price: number; qty: number }[],\n taxRate: number,\n ): { subtotal: number; tax: number; total: number } {\n const subtotal = lineItems.reduce((s, i) => s + i.price * i.qty, 0);\n const tax = subtotal * taxRate;\n return { subtotal, tax, total: subtotal + tax };\n }\n```\n\nThe `): { subtotal: number; tax: number; total: number }` annotation is\nrequired and IS this step's output schema. Downstream steps autocomplete\nagainst it and reference its fields via `{{ steps.<this>.output.<field> }}`,\nand the worker validates the actual return value against it. Describe what\nthe function really returns: a too-loose annotation like `unknown` produces\nan empty schema, which makes downstream `output.<field>` references\nunresolvable and trips \"field not found\" warnings.\n\nThe script runs in a WASM sandbox: 5s wall clock, 10 MB heap, no network,\nno filesystem. Trips show up as `script_timeout` / `script_memory_limit`\non the step execution.\n\n## Be specific with types\n\nEvery output schema you write is read three times: by the LLM (for\n`ai.extract`), by downstream steps (autocomplete + template resolution),\nand by the runtime validator. **The more specific the type, the better\nthe workflow runs.**\n\n### Use `enum` for coded values\n\nCategorical fields like `category`, `status`, `kind`, `severity`,\n`monitoring`, `frequency` should be enums whenever the value set is\nclosed. The LLM is constrained to emit only allowed values, and\ndownstream `transform.script` steps know exactly what they'll see.\n\n```yaml\n- name: classify\n type: ai.extract\n with:\n input: '{{ input.text }}'\n schema:\n type: object\n properties:\n category:\n type: string\n enum: ['901', '902']\n description: |\n Covenant category.\n 901 = Odkladacie podmienky (conditions precedent).\n 902 = Následné podmienky (subsequent / ongoing covenants).\n required: [category]\n```\n\nPer-value meanings go inline in the field's `description`. The LLM picks\nthem up reliably from there, and the scope browser renders the allowed\nvalues when downstream steps reference the field.\n\n### `transform.script` — TS literal unions become JSON Schema enums\n\nA TS literal-union return type compiles to a JSON-Schema enum\nautomatically. Use it when the function returns a value from a closed set:\n\n```yaml\n- name: severity\n type: transform.script\n with:\n inputs:\n score: '{{ steps.score.output.value }}'\n function: |\n function script(score: number): { level: 'low' | 'medium' | 'high' } {\n return { level: score > 0.8 ? 'high' : score > 0.4 ? 'medium' : 'low' };\n }\n```\n\nDownstream steps autocomplete `low | medium | high` for `level`.\n\n### `eigenpal workflow push` warns on weak types\n\nPush surfaces non-fatal **schema-quality warnings** when it sees patterns\nthat hurt downstream typing. Common ones:\n\n| Warning code | What it means |\n| ---------------------------- | -------------------------------------------------------------------------------------------- |\n| `categorical-missing-enum` | A field named like `category` / `status` / `kind` is a plain `string`. Consider declaring an `enum`. |\n| `weak-script-return` | A `transform.script` returns `any` or `unknown`. Replace with a concrete shape or a literal union. |\n| `untyped-object` | A `type: object` field has no `properties`. The LLM has no guidance on what to emit. |\n| `untyped-array-items` | A `type: array` field has no `items:` declared. |\n| `unknown-step-reference` | A Liquid expression references a step that is not in scope, often because it is inside a control container. |\n\nWarnings do not block the push. They print to stderr as `! …` lines. Run\n`eigenpal workflow validate` locally to surface them before pushing.\n\n## Common patterns\n\n### Conditional branches\n\n```yaml\n- name: classify\n type: ai.extract\n with:\n input: '{{ input.text }}'\n schema:\n type: object\n properties:\n kind: { type: string, enum: [invoice, contract, other] }\n\n- name: extract-invoice\n type: ai.extract\n if: '{{ steps.classify.output.kind == \"invoice\" }}'\n with: { ... }\n\n- name: extract-contract\n type: ai.extract\n if: '{{ steps.classify.output.kind == \"contract\" }}'\n with: { ... }\n```\n\n### Map over an array input\n\n```yaml\n- name: extract-each\n type: ai.extract\n forEach: '{{ input.documents }}'\n with:\n input: '{{ item.text }}'\n schema: { ... }\n# steps.extract-each.output is an array of {invoiceNumber, totalAmount, ...}\n```\n\n### Parallel fan-out\n\n```yaml\n- name: extract-fields\n type: control.parallel-map\n forEach: '{{ input.fieldNames }}'\n with:\n steps:\n - name: extract-one\n type: ai.extract\n with:\n input: '{{ input.document }}'\n schema: { type: object, properties: { value: { type: string } } }\n```\n\n### Split a long doc into named sections, then extract each\n\nFor documents that bundle multiple sub-documents (e.g. a contract pack with\nseveral annexes), use `ai.split` to find section boundaries with an LLM,\nthen `control.parallel_map` to run section-specific `ai.extract`. The\nsplitter operates on parsed pages, so DOCX / RTF / scans all reduce to the\nsame per-page representation.\n\nDescription-writing tips (mirrors Reducto's guidance):\n\n- Use the document's own terminology — a Slovak contract calls it `Príloha`,\n not \"annex\". List the multilingual variants you have seen.\n- Mention stable visual cues if any — a centered title-page header, a\n signature line, the start of a specific table.\n- Be specific. \"The collateral schedule table at the start of Príloha 2\"\n beats \"annex\".\n\n```yaml\n- name: parse\n type: ai.parse\n with:\n input: '{{ input.document }}'\n\n- name: split\n type: ai.split\n with:\n input: '{{ steps.parse.output }}'\n sections:\n - name: intro\n description: >-\n The introductory cover/preamble of the loan pack — borrower details,\n loan purpose, signature block. Always before any \"Príloha\".\n - name: priloha_2\n description: >-\n \"Príloha č. 2 / Anlage 2 / Załącznik nr 2\" — the collateral\n schedule. Starts with a section header on its own page; contains\n a table of pledged assets.\n required: true # warn if not found\n endHints: # LLM-judged end cues — phrases the model treats semantically\n - \"PRÍLOHA Č. 3\"\n - \"ANLAGE 3\"\n - name: priloha_3\n description: >-\n \"Príloha č. 3 / Anlage 3\" — the repayment schedule. Tabular\n amortization plan with monthly rows.\n rules: |\n End-of-section markers like *Koniec prílohy 2* close the current section.\n Repeated headers/footers are continuation, not new sections.\n\n- name: extract-each-section\n type: control.parallel_map\n items: '{{ steps.split.output.splits }}'\n as: section\n steps:\n - name: extract-section-fields\n type: ai.extract\n with:\n input: '{{ section.text }}'\n schema:\n type: object\n properties:\n section_name: { type: string }\n key_facts: { type: array, items: { type: string } }\n```\n\nEach `splits[i]` carries `{ name, page_range, confidence, notes, evidence,\nend_evidence, text, pages, pages_anchored, pages_inferred }`.\n\n- `confidence` is `'low' | 'medium' | 'high'` — coarse enum, not a 0..1\n number. LLMs cluster numeric scores meaninglessly; the enum is reliable.\n- `evidence.start_heading_text` is the verbatim heading the LLM cited (parse\n this instead of regexing over `notes`).\n- `end_evidence` is set when the LLM detected a section close (matched\n against the section's `endHints` or an explicit closing marker like\n `*Koniec prílohy*`). Carries `{ end_page, confidence, notes }`. Absent\n when continuity-fill alone closed the section. Pair `evidence.notes` +\n `end_evidence.notes` for a reconciliation pass that judges whether the\n section is correctly bounded.\n- `pages[i].source` is `'anchored'` (direct LLM evidence) or `'inferred'`\n (filled by continuity) — use it to deprioritize inferred content.\n\nPages that match no section are silently dropped (Reducto-style); add an\nexplicit \"everything else\" section if you need a catch-all.\n\nPer-section knobs:\n\n- `required: true` — log a warn if missing and lean the LLM toward expecting\n it. Default: false.\n- `endHints: [string]` — natural-language cues the LLM uses to find the\n section's END. Treated semantically (casing variants, missing diacritics,\n multilingual phrasings all qualify) — this is NOT a regex match. The LLM\n emits a structured `end_anchor`; deterministic merge applies it. Replaces\n the older `endAnchors` line-prefix matcher.\n\n`extract-each-section.output` is `{ items, count, totalIterations }`, not a\nflat array — `items[i]` holds the per-iteration result keyed by the last\ninner step's name (here `{ \"extract-section-fields\": {...} }`).\n\n## Validate before pushing\n\n```bash\neigenpal workflow validate ./workflow.yaml\n```\n\nReports field-level issues:\n\n```\n✗ workflow (./workflow.yaml) — 2 issues\n steps.2.config.passThreshold Required\n steps.2.type Invalid step type: 'eval.llm-judg'\n```\n\nPush:\n\n```bash\n# Create a new workflow (version comes from the YAML's top-level `version:` field)\neigenpal workflow push --file workflow.yaml\n\n# Update an existing one — three ways to set the new version:\neigenpal workflow push --file workflow.yaml --workflow-id wf_… # bump in YAML's `version:`\neigenpal workflow push --file workflow.yaml --workflow-id wf_… --set-version 2.0.0 # explicit semver\neigenpal workflow push --file workflow.yaml --workflow-id wf_… --bump patch # auto-bump (patch | minor | major)\n```\n\n`--bump` requires `--workflow-id` (it bumps from the current server\nversion). `--set-version` works with or without `--workflow-id`. The\nYAML's top-level `version:` field is the fallback when neither flag is\npassed — server rejects pushes whose version does not strictly increase.\n\n## Discover step + evaluator types\n\nDo not memorize fields — introspect:\n\n```bash\neigenpal workflow step-type list # full step catalog\neigenpal workflow step-type get <type> # config + output schema for one step type\neigenpal workflow evaluator-type list # full evaluator catalog\neigenpal workflow evaluator-type get <type> # config schema for one evaluator type\n```\n\nBoth catalogs are generated from the canonical Zod schemas in\n`@eigenpal/types`, so they are always in sync with what the deployment\nsupports.\n\n<!-- GENERATED:WORKFLOW_REFERENCE START -->\n## Top-level fields — full reference\n\n_Generated from `WorkflowDefinitionSchema` in `@eigenpal/types/src/workflow/workflow.ts`. Do not hand-edit between the GENERATED fences — run `bun run --cwd packages/cli generate:skill`._\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `name` | string | yes | | URL-safe workflow slug: 1-64 lowercase letters, digits, underscores, or hyphens; must start with a letter or digit. |\n| `version` | string | no | `\"1.0.0\"` | Author-provided semantic version label stored with the definition. |\n| `description` | string | no | | Human-readable purpose of the workflow. |\n| `enabled` | boolean | no | `true` | Whether the workflow may be run. |\n| `triggerMethods` | array<object> | no | `[{\"type\":\"manual\"}]` | Ways this workflow can be invoked: manual, api, or email. |\n| `inputs` | array<object> | no | | Top-level inputs available under `input` in template expressions. |\n| `steps` | array<unknown> | yes | | Ordered workflow steps; names must be unique. |\n| `output` | record<string, string> \\| string | no | | Final output as named field-to-template mappings or one passthrough template expression. |\n| `settings` | object | no | | Workflow-wide timeout and retry defaults. |\n| `defaultModel` | string | no | | Default configured LLM provider id used by AI steps when the step does not select one. |\n\n\n## Per-input fields\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `name` | string | yes | | Top-level input name used as `input.<name>` in template expressions. |\n| `type` | string | yes | | One of string, enum, number, integer, boolean, array, object, or file. |\n| `description` | string | no | | Human-readable input meaning shown to callers. |\n| `required` | boolean | no | `true` | Whether callers must provide this input. |\n| `default` | unknown | no | | Value used when an optional input is omitted. |\n| `values` | array<string> | no | | Closed set of allowed strings when type is enum. |\n| `items` | object | no | | Element definition when type is array. |\n| `properties` | array<unknown> | no | | Recursive field definitions when type is object. |\n| `source` | string | no | | Registered external file resolver for single-tenant string-id file inputs, for example gpfs; valid only with type file. |\n| `mimeType` | string | no | | MIME hint such as application/pdf for a sourced file; mutually exclusive with extension. |\n| `extension` | string | no | | Extension hint such as pdf for a sourced file; mutually exclusive with mimeType. |\n\n\n## Nested input property fields\n\nObject inputs and object array items use this recursive shape. File inputs are top-level only.\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `name` | string | yes | | JSON object key, addressable in templates with dot notation. |\n| `type` | string | yes | | One of string, enum, number, integer, boolean, array, or object. |\n| `description` | string | no | | Human-readable field meaning. |\n| `required` | boolean | no | | Whether this property must be present; defaults to true when omitted. |\n| `values` | array<string> | no | | Allowed strings when type is enum. |\n| `items` | object | no | | Element definition when type is array. |\n| `properties` | array<unknown> | no | | Recursive fields when type is object. |\n\n\n## Trigger method variants\n\n### `manual`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `type` | `\"manual\"` | yes | | Dashboard/manual run trigger. |\n| `enabled` | boolean | no | | Set false to disable dashboard runs; manual triggering is enabled by default. |\n| `inputSchema` | record<string, unknown> | no | | Optional JSON Schema for trigger input. |\n\n\n### `api`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `type` | `\"api\"` | yes | | Public API run trigger. |\n| `inputSchema` | record<string, unknown> | no | | Optional JSON Schema for API input. |\n\n\n### `email`\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `type` | `\"email\"` | yes | | Inbound email trigger. |\n| `whitelist` | object | no | | Optional sender allowlist by exact email address or domain. |\n\n\n## Workflow settings\n\n| Field | Type | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `timeout` | number | no | | Default step timeout in milliseconds. |\n| `retry` | `\"automatic\"` \\| `\"never\"` \\| object | no | | Default durable retry policy for steps. |\n| `retries` | integer | no | | |\n| `retryDelay` | number | no | | |\n\n\n## Complete machine-readable workflow schema\n\nThis JSON Schema is generated from the same Zod schema used to parse workflow YAML. YAML keys and nesting are identical.\n\n```json\n{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"maxLength\": 64,\n \"pattern\": \"^[a-z0-9][a-z0-9_-]*$\",\n \"description\": \"URL-safe workflow slug: 1-64 lowercase letters, digits, underscores, or hyphens; must start with a letter or digit.\"\n },\n \"kind\": {\n \"description\": \"Deprecated compatibility discriminator; omit for new workflows.\",\n \"default\": \"workflow\",\n \"type\": \"string\",\n \"enum\": [\n \"workflow\"\n ]\n },\n \"version\": {\n \"default\": \"1.0.0\",\n \"description\": \"Author-provided semantic version label stored with the definition.\",\n \"type\": \"string\"\n },\n \"description\": {\n \"description\": \"Human-readable purpose of the workflow.\",\n \"type\": \"string\"\n },\n \"enabled\": {\n \"default\": true,\n \"description\": \"Whether the workflow may be run.\",\n \"type\": \"boolean\"\n },\n \"triggerMethods\": {\n \"default\": [\n {\n \"type\": \"manual\"\n }\n ],\n \"description\": \"Ways this workflow can be invoked: manual, api, or email.\",\n \"type\": \"array\",\n \"items\": {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"manual\",\n \"description\": \"Dashboard/manual run trigger.\"\n },\n \"enabled\": {\n \"description\": \"Set false to disable dashboard runs; manual triggering is enabled by default.\",\n \"type\": \"boolean\"\n },\n \"inputSchema\": {\n \"description\": \"Optional JSON Schema for trigger input.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"api\",\n \"description\": \"Public API run trigger.\"\n },\n \"inputSchema\": {\n \"description\": \"Optional JSON Schema for API input.\",\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"email\",\n \"description\": \"Inbound email trigger.\"\n },\n \"whitelist\": {\n \"description\": \"Optional sender allowlist by exact email address or domain.\",\n \"type\": \"object\",\n \"properties\": {\n \"domains\": {\n \"description\": \"Allowed sender domains.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"emails\": {\n \"description\": \"Allowed exact sender email addresses.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"type\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"inputs\": {\n \"description\": \"Top-level inputs available under `input` in template expressions.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"Top-level input name used as `input.<name>` in template expressions.\"\n },\n \"type\": {\n \"type\": \"string\",\n \"description\": \"One of string, enum, number, integer, boolean, array, object, or file.\"\n },\n \"description\": {\n \"description\": \"Human-readable input meaning shown to callers.\",\n \"type\": \"string\"\n },\n \"required\": {\n \"default\": true,\n \"description\": \"Whether callers must provide this input.\",\n \"type\": \"boolean\"\n },\n \"default\": {\n \"description\": \"Value used when an optional input is omitted.\"\n },\n \"values\": {\n \"description\": \"Closed set of allowed strings when type is enum.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"items\": {\n \"description\": \"Element definition when type is array.\",\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"description\": \"Array element type: string, enum, number, integer, boolean, object, or file.\"\n },\n \"values\": {\n \"description\": \"Closed set of allowed strings when items.type is enum.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"properties\": {\n \"description\": \"Recursive fields when items.type is object.\",\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema0\"\n }\n }\n },\n \"required\": [\n \"type\"\n ],\n \"additionalProperties\": false\n },\n \"properties\": {\n \"description\": \"Recursive field definitions when type is object.\",\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema0\"\n }\n },\n \"source\": {\n \"description\": \"Registered external file resolver for single-tenant string-id file inputs, for example gpfs; valid only with type file.\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"mimeType\": {\n \"description\": \"MIME hint such as application/pdf for a sourced file; mutually exclusive with extension.\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"extension\": {\n \"description\": \"Extension hint such as pdf for a sourced file; mutually exclusive with mimeType.\",\n \"type\": \"string\",\n \"minLength\": 1\n }\n },\n \"required\": [\n \"name\",\n \"type\",\n \"required\"\n ],\n \"additionalProperties\": false\n }\n },\n \"steps\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n },\n \"description\": \"Ordered workflow steps; names must be unique.\"\n },\n \"output\": {\n \"description\": \"Final output as named field-to-template mappings or one passthrough template expression.\",\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {\n \"type\": \"string\"\n }\n },\n {\n \"type\": \"string\"\n }\n ]\n },\n \"settings\": {\n \"description\": \"Workflow-wide timeout and retry defaults.\",\n \"type\": \"object\",\n \"properties\": {\n \"timeout\": {\n \"description\": \"Default step timeout in milliseconds.\",\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"description\": \"Default durable retry policy for steps.\",\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n }\n },\n \"additionalProperties\": false\n },\n \"defaultModel\": {\n \"description\": \"Default configured LLM provider id used by AI steps when the step does not select one.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"name\",\n \"version\",\n \"enabled\",\n \"triggerMethods\",\n \"steps\"\n ],\n \"additionalProperties\": false,\n \"definitions\": {\n \"__schema0\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"pattern\": \"^[a-zA-Z_][a-zA-Z0-9_]*$\",\n \"description\": \"JSON object key, addressable in templates with dot notation.\"\n },\n \"type\": {\n \"type\": \"string\",\n \"description\": \"One of string, enum, number, integer, boolean, array, or object.\"\n },\n \"description\": {\n \"description\": \"Human-readable field meaning.\",\n \"type\": \"string\"\n },\n \"required\": {\n \"description\": \"Whether this property must be present; defaults to true when omitted.\",\n \"type\": \"boolean\"\n },\n \"values\": {\n \"description\": \"Allowed strings when type is enum.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"items\": {\n \"description\": \"Element definition when type is array.\",\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"description\": \"Array element type: string, enum, number, integer, boolean, or object.\"\n },\n \"values\": {\n \"description\": \"Allowed strings when items.type is enum.\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"properties\": {\n \"description\": \"Recursive object fields when items.type is object.\",\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema0\"\n }\n }\n },\n \"required\": [\n \"type\"\n ],\n \"additionalProperties\": false\n },\n \"properties\": {\n \"description\": \"Recursive fields when type is object.\",\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema0\"\n }\n }\n },\n \"required\": [\n \"name\",\n \"type\"\n ],\n \"additionalProperties\": false\n },\n \"__schema1\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"ai.parse\",\n \"ai.extract\",\n \"ai.split\",\n \"ai.segment\",\n \"ai.classify\",\n \"ai.classify-pages\",\n \"ai.vision\",\n \"ai.search-files\",\n \"ai.figure-crops\"\n ]\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\",\n \"name\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"transform.set\",\n \"transform.remove\",\n \"transform.combine\",\n \"transform.split\",\n \"transform.merge\",\n \"transform.template\",\n \"transform.pdf-embed\",\n \"transform.crop-regions\",\n \"transform.xlsx-to-json\",\n \"transform.json-to-xlsx\",\n \"transform.script\",\n \"transform.text-chunker\",\n \"transform.regex-extract\",\n \"transform.archive-list\",\n \"transform.archive-extract\"\n ]\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\",\n \"name\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"enum\": [\n \"action.http\",\n \"action.invoke-workflow\",\n \"action.website-reader\",\n \"action.email\"\n ]\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\",\n \"name\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.wait\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"duration\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n }\n },\n \"required\": [\n \"type\",\n \"name\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.human_review\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"with\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.fail\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"condition\": {\n \"type\": \"string\"\n },\n \"statusCode\": {\n \"type\": \"integer\",\n \"minimum\": 400,\n \"maximum\": 599\n },\n \"message\": {\n \"type\": \"string\",\n \"minLength\": 1\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.block\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retry\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"const\": \"inherit\"\n },\n {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n {\n \"type\": \"string\",\n \"const\": \"never\"\n },\n {\n \"oneOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"automatic\"\n },\n \"maxAttempts\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"mode\": {\n \"type\": \"string\",\n \"const\": \"never\"\n }\n },\n \"required\": [\n \"mode\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n ]\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"blockName\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"inputs\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"blockName\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.if\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"condition\": {\n \"type\": \"string\"\n },\n \"then\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n },\n \"else\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"condition\",\n \"then\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.switch\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"on\": {\n \"type\": \"string\"\n },\n \"cases\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"when\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"number\"\n },\n {\n \"type\": \"boolean\"\n }\n ]\n },\n \"steps\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n }\n },\n \"required\": [\n \"when\",\n \"steps\"\n ],\n \"additionalProperties\": false\n }\n },\n \"default\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"on\",\n \"cases\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.parallel\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"branches\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"steps\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n }\n },\n \"required\": [\n \"name\",\n \"steps\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"branches\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.foreach\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"items\": {\n \"type\": \"string\"\n },\n \"as\": {\n \"type\": \"string\"\n },\n \"indexAs\": {\n \"type\": \"string\"\n },\n \"steps\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"items\",\n \"as\",\n \"steps\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"control.parallel_map\"\n },\n \"name\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"description\": {\n \"type\": \"string\"\n },\n \"if\": {\n \"type\": \"string\"\n },\n \"timeout\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"retries\": {\n \"type\": \"integer\",\n \"minimum\": 0,\n \"maximum\": 9007199254740991\n },\n \"retryDelay\": {\n \"type\": \"number\",\n \"exclusiveMinimum\": 0\n },\n \"with\": {\n \"type\": \"object\",\n \"propertyNames\": {\n \"type\": \"string\"\n },\n \"additionalProperties\": {}\n },\n \"items\": {\n \"type\": \"string\"\n },\n \"as\": {\n \"type\": \"string\"\n },\n \"indexAs\": {\n \"type\": \"string\"\n },\n \"concurrency\": {\n \"default\": 5,\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 50\n },\n \"steps\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/__schema1\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"name\",\n \"items\",\n \"as\",\n \"steps\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n }\n}\n```\n<!-- GENERATED:WORKFLOW_REFERENCE END -->\n"
|
|
430
430
|
},
|
|
431
431
|
{
|
|
432
432
|
"topic": "releases/0.10.0",
|
|
@@ -1079,6 +1079,20 @@
|
|
|
1079
1079
|
"source": "public",
|
|
1080
1080
|
"content": "---\ntitle: '0.17.5'\nversion: '0.17.5'\ndate: '2026-09-21'\ncommit: '3a7d7f7e9a0142c9342653cd3950bb028322e8d1'\ndescription: 'EigenPal platform release 0.17.5'\n---\n\n## Platform (Studio)\n\n- Summary Agent: spreadsheet uploads now use a hardened parser that prevents crafted workbook content from causing excessive processing or modifying object prototypes.\n\n## CLI\n\n- CLI skill reference documents the new `ai.figure-crops` workflow step for cropping and captioning figures from parsed PDFs.\n\n## SDK\n\n- Python SDK: installs now require `anyio` 4.14.2 or newer. Earlier versions encoded host names with IDNA 2003 when opening a TLS connection, so a crafted host name could be matched against the wrong certificate. The TypeScript SDK is unaffected.\n"
|
|
1081
1081
|
},
|
|
1082
|
+
{
|
|
1083
|
+
"topic": "releases/0.18.0",
|
|
1084
|
+
"title": "0.18.0",
|
|
1085
|
+
"description": "EigenPal platform release 0.18.0",
|
|
1086
|
+
"source": "public",
|
|
1087
|
+
"content": "---\ntitle: '0.18.0'\nversion: '0.18.0'\ndate: '2026-09-21'\ncommit: '78ec70a2c1fd4d43dd334b5c118387a67e1c7888'\ndescription: 'EigenPal platform release 0.18.0'\n---\n\n## Platform (Studio)\n\n- Workflows can crop normalized regions from PDFs and images into JPEG artifacts with the new `transform.crop-regions` step — pair it with parse output and script steps to build figure or layout evidence pipelines without a figure-specific step type. Turn on optional **`cache: true`** on **`ai.parse`** to reuse prior parse output for the same file bytes and parse settings across runs (stored in tenant blob storage; no expiry yet).\n"
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
"topic": "releases/0.19.0",
|
|
1091
|
+
"title": "0.19.0",
|
|
1092
|
+
"description": "EigenPal platform release 0.19.0",
|
|
1093
|
+
"source": "public",
|
|
1094
|
+
"content": "---\ntitle: '0.19.0'\nversion: '0.19.0'\ndate: '2026-09-21'\ncommit: '741470ef1ded25c637ead637f059716b3c8f7534'\ndescription: 'EigenPal platform release 0.19.0'\n---\n\n## Platform (Studio)\n\n- Agent builder sessions can now be given storage credentials limited to their own session folder instead of the whole storage account. Set a Cloudflare account id and API token, or an AWS role, and each training sandbox receives a short-lived key that can only read and write its own files. Deployments that do not configure this keep working exactly as before and log a warning, and a new setting makes the platform refuse to start a sandbox unless scoped credentials are available.\n- Studio security hardening. Parsed document text shown in run traces and grounding views is now sanitized before rendering, so a crafted document can no longer inject active content into the page. Observability settings now require administrator access to view, and the values of any authorization headers you configure are masked once saved, so they are never sent back to the browser. Saving a renamed header now asks you to retype its value instead of quietly clearing the configuration. Outbound observability destinations are validated to reject internal and unencrypted endpoints on hosted plans. Editor assets are now served from Eigenpal's own domain instead of a public content network. Self-hosted deployments that use password sign-in no longer accept open self-registration. Creating the first account still works, and anyone holding an invitation can still create their account, but an uninvited visitor can no longer join an existing workspace. Set `EIGENPAL_ALLOW_OPEN_SIGNUP=true` to restore the previous behavior. Agent training and email trigger surfaces are now scoped more tightly. Email alias changes are accepted only through the agent that owns the alias, so editing or revoking one from a different agent's page now reports not found. Opening a training session that belongs to a different workspace or a different agent is refused instead of returning a server error. Each agent workspace now runs with credentials valid only for its own session, so a workspace can no longer act on any session but its own. Sign-in and account safety. Changing your password now signs out your other sessions, so a password change ends access from a device you no longer control. Accepting an invitation now requires the invitation link itself rather than just the invited address, so knowing that someone was invited is no longer enough to create an account in their name. Requests that arrive from another site are refused, which closes cross-site request forgery against pages you are signed in to. Set `EIGENPAL_TRUSTED_ORIGINS` if you serve Eigenpal on more than one hostname.\n\n## Public API\n\n- Agent endpoints that accept an agent id now resolve it strictly within the calling organization. Requesting an id belonging to another organization returns not found instead of reading or modifying it. Run and step endpoints apply the same organization check to execution ids. Uploads that omit a declared length are now measured while they stream and return 413 once they exceed the request limit, rather than being read into memory first. Operator source tokens are accepted only on the source and authentication check endpoints they are documented for. Webhook endpoints are now checked when you save them, not only when a delivery is attempted. A destination the platform cannot deliver to is rejected with a validation error at creation and update time, instead of being accepted and then failing silently on every send. Self-hosted deployments that deliver to their own internal network are unaffected. Every endpoint that starts a new run now honours the automation's trigger settings, the same way `POST /api/v1/runs` already did. That covers rerunning a run, starting an experiment, and running a single dataset example, including their streaming variants. An API key calling one of these on an automation whose API trigger is turned off now receives `403 api_trigger_disabled`, and a dashboard session calling one on an API-only automation receives `403 manual_trigger_disabled`. If an existing integration starts returning 403, turn the API trigger back on for that automation.\n- Self-hosted Prometheus scrapes now require the dedicated scrape token in every deployment. An organization API key is no longer enough to read the cluster snapshot. A deployment without `EIGENPAL_METRICS_SCRAPE_TOKEN` returns 404 on the metrics route, so set the token before upgrading if you scrape today with an API key. The headless workflow server no longer serves a network address without an API token. If you set `HOST` yourself, the server now exits at startup unless you also set `HEADLESS_API_TOKEN` or `HEADLESS_ALLOW_UNAUTHENTICATED_BIND=1`. If you never set `HOST`, the bind address changes from `0.0.0.0` to `127.0.0.1`, which inside a container means a published port stops accepting connections while the container still reports healthy. Either set `HEADLESS_API_TOKEN` (16 characters or more) together with `HOST=0.0.0.0`, or set `HEADLESS_ALLOW_UNAUTHENTICATED_BIND=1` to keep the previous behaviour. Members can no longer pull decrypted agent environment secrets. Use an owner or admin key, or decrypt a secret for a specific run.\n\n## Infrastructure & processing\n\n- Self-hosted Prometheus scrapes now require the dedicated scrape token in every deployment. An organization API key is no longer enough to read the cluster snapshot. A deployment without `EIGENPAL_METRICS_SCRAPE_TOKEN` returns 404 on the metrics route, so set the token before upgrading if you scrape today with an API key. The headless workflow server no longer serves a network address without an API token. If you set `HOST` yourself, the server now exits at startup unless you also set `HEADLESS_API_TOKEN` or `HEADLESS_ALLOW_UNAUTHENTICATED_BIND=1`. If you never set `HOST`, the bind address changes from `0.0.0.0` to `127.0.0.1`, which inside a container means a published port stops accepting connections while the container still reports healthy. Either set `HEADLESS_API_TOKEN` (16 characters or more) together with `HOST=0.0.0.0`, or set `HEADLESS_ALLOW_UNAUTHENTICATED_BIND=1` to keep the previous behaviour. Members can no longer pull decrypted agent environment secrets. Use an owner or admin key, or decrypt a secret for a specific run.\n- Document and archive processing is more resilient to malformed or hostile input. Dataset archive imports now apply a limit on entry count and hold far less memory while expanding, so a small archive can no longer exhaust a request. Office documents are checked against their real expanded size rather than the size recorded inside the file, which blocks highly compressed payloads that previously passed inspection. PDF page rendering moved to a newer engine that removes a document driven code execution path. Script transform steps have a shorter maximum run time that is now capped relative to worker health checks, so a long running script cannot stall other work. With stock settings the ceiling drops from 30 seconds to 15 seconds; self-hosted deployments that need more can raise `HEALTH_LIVENESS_STALE_MS`, since the ceiling is half that value. Workflow processing is more resilient to hostile input. Template rendering now has time and memory ceilings, so a crafted template can no longer stall a worker and delay other runs. Deep merges in the combine transform can no longer be steered into altering shared state. External file sources reject addresses that are not plain HTTP endpoints.\n"
|
|
1095
|
+
},
|
|
1082
1096
|
{
|
|
1083
1097
|
"topic": "releases/0.5.10",
|
|
1084
1098
|
"title": "0.5.10",
|
|
@@ -1385,7 +1399,7 @@
|
|
|
1385
1399
|
"title": "Parse Document",
|
|
1386
1400
|
"description": "Extract text from documents (PDF, DOCX, images) using native extraction, OCR, or vision models",
|
|
1387
1401
|
"source": "public",
|
|
1388
|
-
"content": "---\ntitle: \"Parse Document\"\ndescription: \"Extract text from documents (PDF, DOCX, images) using native extraction, OCR, or vision models\"\n---\n\n`ai.parse` turns a document into text. It is almost always the first step in a\ndocument-understanding workflow: point it at an uploaded file, get back clean\ntext plus per-page content, then feed that into [`ai.extract`](/steps/ai/extract),\n[`ai.split`](/steps/ai/split), or a [`transform.script`](/steps/transform/script).\n\n## When to use it\n\n- **You have a PDF, image, or Office document and need its text.** `ai.parse`\n handles PDFs, images (PNG/JPG), and Office formats (DOCX, PPTX, XLSX, ODT, and\n more), normalizing them to one text representation.\n- **You need per-page boundaries.** The `pages` array carries page-scoped text so\n downstream steps can cite evidence by page or split a document into sections.\n- **You need document geometry or spatial extraction.** Layout-aware OCR also\n returns normalized `pages[].layoutElements` and, when the provider supports\n it, a canonical OpenParser `ParsedDocument` on `structured` — blocks,\n regions, tables, figures, chunks, polygons, and bounding boxes. Pass the\n whole parse output (not just `.text`) into downstream `ai.extract` to enable\n shared grounded extraction, spatial citations, and lineage sidecars.\n- **You need strict data-egress control.** Use `parseMode: native` when embedded\n PDF text is enough and document bytes must not leave your deployment for OCR or\n vision. Use `parseMode: native-or-ocr` when most pages have good embedded text\n but some pages show objectively detectable extraction anomalies.\n- **You need column-aligned native PDF text.** Use `outputFormat: layout` with\n native extraction when markdown would flatten multi-column tables or forms and\n you want fixed-width plain text with horizontal gaps preserved.\n\n## How parsing is chosen\n\nThe step picks a strategy based on the document and your config:\n\n### Native (`parseMode: native`)\n\nLocal extraction only — embedded PDF text is read in the worker with **no OCR or\nvision egress**. There are **no parsing credits**. Raster images (PNG, JPG, TIFF,\nand similar) are **not supported** in this mode. **`describeFigures` is\nincompatible** — figure descriptions require a vision pass.\n\nNative extraction is **not a universal OCR replacement**. PDFs can render glyphs\nwithout usable Unicode mappings; diagnostics report detectable problems (empty\npages, replacement characters, forbidden controls, unassigned/noncharacter code\npoints, heavy private-use) but cannot certify that valid-looking text is\nsemantically correct. If **every** page is empty or suspect, the step **fails**\ninstead of returning unusable text or silently calling OCR. Mixed documents keep\nthe native text and attach per-page `nativeTextQuality`.\n\nOffice formats and plain text still use native extraction. Paginated word-processing\nand presentation formats use headless LibreOffice when per-page boundaries are\nneeded. Spreadsheets are read directly: each non-empty sheet becomes a named page,\nincluding legacy and macro-enabled Excel workbooks. Formulas and macros are never\nexecuted. Blank sheets are omitted and `pageIndex` counts the returned non-empty\nsheets, so use `pageName` when you need the workbook's sheet identity.\n\n### Spatial layout text (`outputFormat: layout`)\n\nReturns the same `text` and `pages[].text` fields as other formats, but as\nfixed-width plain text that preserves column spacing and line breaks from the\nPDF page layout. Use it for multi-column forms, tables, or address blocks where\n`markdown` (the default) would collapse horizontal alignment.\n\nConstraints:\n\n- **PDF only** — Office files, images, and plaintext fail. There is no\n Kreuzberg or OCR substitute for those types.\n- **Explicit native parse mode** — set `parseMode: native` or\n `parseMode: native-or-ocr`. Omitted `parseMode`, OCR, vision, and\n `nativeText: true` reject `layout`.\n- **Native mode is fail-closed** — every page is spatial layout text. If\n extraction fails, the step fails.\n- **Hybrid output can mix formats** — `native-or-ocr` keeps layout text on\n accepted native pages and uses the OCR provider's markdown on suspect or\n empty pages.\n- **Extractor failure is fail-closed** — a Poppler process error never\n silently routes to Kreuzberg or OCR.\n- **Other formats unchanged** — `markdown`, `plain`, `djot`, and `html` behave\n as before; `markdown` remains the default.\n\nLocal development requires `pdftotext` on `PATH` (`brew install poppler` on\nmacOS, `poppler-utils` on Linux). Managed deployments include this dependency.\n\n```yaml\n- name: parse\n type: ai.parse\n with:\n input: '{{ input.document }}'\n parseMode: native\n outputFormat: layout\n```\n\n### Native with OCR fallback (`parseMode: native-or-ocr`)\n\nStarts with the same local native extraction, then runs **page-quality diagnostics**\non each PDF page. Pages with **objectively detectable anomalies** — empty text,\nU+FFFD replacement characters, lone UTF-16 surrogates, forbidden controls,\nunassigned/noncharacter code points, or heavy private-use areas — trigger the\nconfigured OCR engine.\nValid-looking wrong mappings and literal `?` characters are **not** flagged; native\ndiagnostics report detectable issues but do **not** certify semantic correctness.\n\nWhen the OCR provider supports page selection, only fallback pages are processed\nand billed. Page selection does not imply reduced-byte egress: a provider may\nstill receive the original document bytes. When page selection is unavailable,\nthe provider processes and bills the **whole document**. Only detected fallback\npages replace native pages in the merged output; clean native pages are retained.\nWith `outputFormat: layout`, retained native pages stay as fixed-width layout\ntext and replaced pages use OCR markdown.\n\nYou pay **OCR credits for pages actually processed by the provider** — see\n`output.usage.ocrPagesProcessed` (which may reflect a whole-document OCR call even\nwhen only some pages replace native text). Per-page provenance is recorded on each\npage (`pages[].provenance`).\n\n### OCR (`parseMode: ocr`, the default)\n\nRuns optical character recognition for scanned PDFs and images. Select the engine\nwith `ocrModel`. Document bytes are sent to the OCR provider — **billed per\npage** according to the provider's credit rate.\n\n### Vision AI (`parseMode: vision`)\n\nReads rendered page images with the selected `llmModel`. Page images are sent to\nthe vision provider — **billed per page**. Use it for layouts where semantic\nvisual understanding matters. `pagesPerBatch` and `maxConcurrency` tune how pages\nare batched.\n\n### Legacy document-wide native text (`nativeText: true`)\n\nWhen set on **OCR or Vision** configs, the worker tries embedded PDF text first\nand falls back to the selected parser for the **whole document** if native text\nis missing. This is legacy behavior — prefer explicit `parseMode: native` or\n`native-or-ocr` for predictable egress and billing.\n\n### Figure descriptions (`describeFigures: true`)\n\nOptional vision pass **after** text extraction. With OCR or hybrid native+OCR,\n`figureModel` chooses the caption model; with Vision AI, the selected `llmModel`\nis reused. **Unavailable in Native mode.** The layout scan and caption calls are\nbilled separately and send page images to the vision provider.\n\nPaginated Office formats are converted to PDF first (via headless LibreOffice) so\nthey get true per-page boundaries instead of a single blob. Spreadsheets stay\nworkbook-native to avoid cropping wide sheets.\n\n## Data egress and cost\n\n| Mode | Document egress | Typical cost |\n| --- | --- | --- |\n| `native` | None for parsing | No parsing credits |\n| `native-or-ocr` | Capability-dependent: the provider may receive the full document even when it processes selected pages; without page selection it receives/processes the whole document | OCR credits × `usage.ocrPagesProcessed` (provider pages dispatched) |\n| `ocr` | OCR provider receives document/pages | OCR credits × page count |\n| `vision` | Vision provider receives rendered page images | Vision credits × page count |\n| `describeFigures` | Vision provider receives figure pages (additive) | Vision credits for captioned pages |\n\nThere is **one combined text output** — the worker does not emit separate native\nand OCR text streams.\n\n## Example\n\n```yaml\n- name: parse\n type: ai.parse\n with:\n input: '{{ input.document }}'\n parseMode: native-or-ocr\n ocrModel: openparser-default\n```\n\nFor a digital PDF with one page of garbled embedded text, most pages stay native\nin the merged output. OpenParser currently processes the whole document for OCR,\nso every page incurs OCR processing and credits. Azure can process and bill only\nthe anomalous page, but the current Azure adapter still uploads the original PDF\nbytes; page selection is not a subset-egress guarantee.\n\n## Configuration\n\nConfiguration goes inside the step's `with:` block.\n\n{/* GENERATED:CONFIG START */}\n<ParamField path=\"input\" type=\"string\" required>\nStorage reference or template expression for the document\n</ParamField>\n<ParamField path=\"parseMode\" type=\""ocr" | "vision" | "native" | "native-or-ocr"\">\nBase parser for PDF/image inputs. OCR is the default; vision uses the selected LLM. `native` extracts PDF embedded text only and never calls OCR or vision. `native-or-ocr` extracts native text, runs page-quality diagnostics, and requests OCR when pages have detectable anomalies (empty, U+FFFD, lone surrogates, forbidden controls, unassigned/noncharacter, heavy PUA). Page selection, subset egress, and billing are provider-dependent. Valid-looking wrong text and literal \"?\" are not flagged. Text and Office files always use local parsing.\n</ParamField>\n<ParamField path=\"ocrModel\" type=\"string\">\nOCR provider ID for PDF/image parsing\n</ParamField>\n<ParamField path=\"llmModel\" type=\"string\">\nLLM provider ID for vision-based parsing\n</ParamField>\n<ParamField path=\"llmReasoningEffort\" type=\""none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"\">\nReasoning effort for models that support it. Omit to use the selected model's default.\n</ParamField>\n<ParamField path=\"figureModel\" type=\"string\">\nVision model used only for the optional figure-description pass\n</ParamField>\n<ParamField path=\"figureReasoningEffort\" type=\""none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"\">\nReasoning effort for models that support it. Omit to use the selected model's default.\n</ParamField>\n<ParamField path=\"maxConcurrency\" type=\"number\" default=\"3\">\nMax concurrent VLM batch requests\n</ParamField>\n<ParamField path=\"pagesPerBatch\" type=\"number\" default=\"5\">\nNumber of page images per VLM request\n</ParamField>\n<ParamField path=\"pdfRenderScale\" type=\"number\" default=\"1\">\nScale factor for rendering PDF pages before VLM parsing. Higher values produce sharper images at larger payload sizes.\n</ParamField>\n<ParamField path=\"imageQuality\" type=\"integer\" default=\"85\">\nJPEG quality for rendered PDF page images sent to VLM parsing. Higher values reduce compression artifacts at larger payload sizes.\n</ParamField>\n<ParamField path=\"prompt\" type=\"string\">\nCustom extraction prompt\n</ParamField>\n<ParamField path=\"languages\" type=\"array<string>\">\nOCR language hints\n</ParamField>\n<ParamField path=\"outputFormat\" type=\""plain" | "markdown" | "djot" | "html" | "layout"\" default=\"markdown\">\nFormat for extracted text. `markdown` (default) keeps structure; `plain` is unstyled text; `djot`/`html` preserve more markup. `layout` is native-PDF spatial text with column gaps preserved as spaces. It requires `parseMode: native` or `parseMode: native-or-ocr` and a PDF, omitted parseMode and `nativeText: true` are not enough. Native mode is layout-only and fail-closed. `native-or-ocr` keeps layout text on accepted native pages and uses OCR markdown on suspect or empty pages, so the document can mix fixed-width layout and markdown. Extractor failure never falls back to another parser. Office, plaintext, and images fail. OCR/vision cannot request `layout`.\n</ParamField>\n<ParamField path=\"nativeText\" type=\"boolean\" default=\"false\">\nExtract native/embedded text from PDFs without OCR/VLM. Faster and uses no credits. Falls back to OCR/VLM if the PDF has no embedded text.\n</ParamField>\n<ParamField path=\"describeFigures\" type=\"boolean\">\nOpt-in (default off). After text extraction, detect which pages contain figures with an in-worker layout model, then caption those pages with a vision model and append `<figure>description</figure>` to their text, so image-only pages (property photos, signatures, charts) become findable by text-based steps like ai.split. Note: the layout scan runs over all pages, and the caption step and its vision calls are billed. Skipped for plaintext.\n</ParamField>\n<ParamField path=\"figureInstructions\" type=\"string\">\nCustom instruction for the figure-description pass, e.g. \"Describe each figure; label a handwritten signature as `<figure>signature</figure>` and a stamp as `<figure>stamp</figure>`; for property photos note the room or exterior shown.\" Applied only when describeFigures runs.\n</ParamField>\n{/* GENERATED:CONFIG END */}\n\n## Output\n\nAfter a run, inspect **`processingStrategy`** for the resolved path: `native`,\n`ocr`, `vision`, or `hybrid` (native pages merged with OCR on fallback pages).\nEach page may include **`provenance`** (`native` or `ocr`) and\n**`nativeTextQuality`** when native diagnostics ran — diagnostics report\ndetectable anomalies only, not semantic correctness.\n**`usage.ocrPagesProcessed`** counts pages billed through the OCR provider on\nhybrid runs.\n\n**`structured`** carries an OpenParser `ParsedDocument` (`openparser@1`) when\nthe parser produced one. Downstream `ai.extract` validates it at runtime; invalid\nor absent structured data keeps text-only extraction and text grounding.\n\nPer-page provenance badges in Studio run detail are planned; until then use these\noutput fields in templates, CLI inspection, or the execution JSON view.\n\n{/* GENERATED:OUTPUT START */}\n<ResponseField path=\"document\" type=\"object\" required>\n<Expandable title=\"document properties\">\n<ResponseField path=\"filename\" type=\"string\" required>\n</ResponseField>\n<ResponseField path=\"mimeType\" type=\"string\" required>\n</ResponseField>\n<ResponseField path=\"size\" type=\"number\" required>\nFile size in bytes\n</ResponseField>\n<ResponseField path=\"storageRef\" type=\"string\">\nStorage reference for the original file\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"usage\" type=\"object\" required>\n<Expandable title=\"usage properties\">\n<ResponseField path=\"pageCount\" type=\"number\" required>\n</ResponseField>\n<ResponseField path=\"processingTimeMs\" type=\"number\">\n</ResponseField>\n<ResponseField path=\"ocrPagesProcessed\" type=\"integer\">\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"pages\" type=\"array<object>\" required>\n<Expandable title=\"pages properties\">\n<ResponseField path=\"pageIndex\" type=\"number\" required>\n0-based page index\n</ResponseField>\n<ResponseField path=\"text\" type=\"string\" required>\nExtracted page text (markdown/HTML/plain, or spatial layout text)\n</ResponseField>\n<ResponseField path=\"pageName\" type=\"string\">\nPage/sheet name (e.g., Excel sheet name)\n</ResponseField>\n<ResponseField path=\"layoutElements\" type=\"array<object>\">\nSemantic layout elements in reading order\n<Expandable title=\"layoutElements properties\">\n<ResponseField path=\"type\" type=\""paragraph" | "title" | "sectionHeading" | "pageHeader" | "pageFooter" | "pageNumber" | "footnote" | "table" | "figure" | "formulaBlock"\" required>\n</ResponseField>\n<ResponseField path=\"content\" type=\"string\" required>\n</ResponseField>\n<ResponseField path=\"table\" type=\"object\">\n</ResponseField>\n<ResponseField path=\"caption\" type=\"string\">\n</ResponseField>\n<ResponseField path=\"figureId\" type=\"string\">\n</ResponseField>\n<ResponseField path=\"boundingRegion\" type=\"object\">\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"words\" type=\"array<object>\">\nWord-level positions\n<Expandable title=\"words properties\">\n<ResponseField path=\"text\" type=\"string\" required>\n</ResponseField>\n<ResponseField path=\"confidence\" type=\"number\">\n</ResponseField>\n<ResponseField path=\"boundingRegion\" type=\"object\">\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"lines\" type=\"array<object>\">\nLine-level positions\n<Expandable title=\"lines properties\">\n<ResponseField path=\"text\" type=\"string\" required>\n</ResponseField>\n<ResponseField path=\"confidence\" type=\"number\">\n</ResponseField>\n<ResponseField path=\"boundingRegion\" type=\"object\">\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"tables\" type=\"array<object>\">\nExtracted tables\n<Expandable title=\"tables properties\">\n<ResponseField path=\"cells\" type=\"array<object>\" required>\n</ResponseField>\n<ResponseField path=\"rowCount\" type=\"number\" required>\n</ResponseField>\n<ResponseField path=\"columnCount\" type=\"number\" required>\n</ResponseField>\n<ResponseField path=\"boundingRegion\" type=\"object\">\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"width\" type=\"number\">\nPage width\n</ResponseField>\n<ResponseField path=\"height\" type=\"number\">\nPage height\n</ResponseField>\n<ResponseField path=\"unit\" type=\""pixel" | "inch" | "point" | "normalized"\">\nUnit for width/height and bounding regions\n</ResponseField>\n<ResponseField path=\"confidence\" type=\"number\">\nOverall page confidence\n</ResponseField>\n<ResponseField path=\"nativeTextQuality\" type=\"object\">\nPer-page diagnosis of native-extracted text. Reports objectively detectable anomalies (empty, U+FFFD, forbidden controls, unassigned/noncharacter code points, heavy private-use). Does not certify semantic correctness, valid-looking wrong mappings and literal \"?\" cannot be distinguished from legitimate text.\n<Expandable title=\"nativeTextQuality properties\">\n<ResponseField path=\"status\" type=\""clean" | "empty" | "suspect"\" required>\n</ResponseField>\n<ResponseField path=\"reasons\" type=\"array<"empty" | "replacement-character" | "lone-surrogate" | "forbidden-control" | "unassigned" | "noncharacter" | "private-use">\" required>\n</ResponseField>\n<ResponseField path=\"counts\" type=\"object\" required>\n</ResponseField>\n<ResponseField path=\"profile\" type=\"object\" required>\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"provenance\" type=\"object\">\nWhich extractor produced the final text/layout on this page\n<Expandable title=\"provenance properties\">\n<ResponseField path=\"source\" type=\""native" | "ocr" | "vision"\" required>\n</ResponseField>\n</Expandable>\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"text\" type=\"string\" required>\nCombined text from all pages\n</ResponseField>\n<ResponseField path=\"parserType\" type=\""plaintext" | "office" | "llm-vision" | "ocr"\" required>\n</ResponseField>\n<ResponseField path=\"parserVersion\" type=\"string\">\n</ResponseField>\n<ResponseField path=\"model\" type=\"string\">\nModel used (for LLM/OCR parsers)\n</ResponseField>\n<ResponseField path=\"processingStrategy\" type=\""native" | "ocr" | "vision" | "hybrid"\">\nHow this result was produced: `native` (local PDF text only), `ocr`, `vision`, or `hybrid` (native pages merged with OCR on fallback pages).\n</ResponseField>\n<ResponseField path=\"structured\" type=\"record<string, unknown>\">\nCanonical structured document with ordered blocks, regions, bounding boxes, tables, figures, and chunks when supported by the parser\n</ResponseField>\n{/* GENERATED:OUTPUT END */}\n"
|
|
1402
|
+
"content": "---\ntitle: \"Parse Document\"\ndescription: \"Extract text from documents (PDF, DOCX, images) using native extraction, OCR, or vision models\"\n---\n\n`ai.parse` turns a document into text. It is almost always the first step in a\ndocument-understanding workflow: point it at an uploaded file, get back clean\ntext plus per-page content, then feed that into [`ai.extract`](/steps/ai/extract),\n[`ai.split`](/steps/ai/split), or a [`transform.script`](/steps/transform/script).\n\n## When to use it\n\n- **You have a PDF, image, or Office document and need its text.** `ai.parse`\n handles PDFs, images (PNG/JPG), and Office formats (DOCX, PPTX, XLSX, ODT, and\n more), normalizing them to one text representation.\n- **You need per-page boundaries.** The `pages` array carries page-scoped text so\n downstream steps can cite evidence by page or split a document into sections.\n- **You need document geometry or spatial extraction.** Layout-aware OCR also\n returns normalized `pages[].layoutElements` and, when the provider supports\n it, a canonical OpenParser `ParsedDocument` on `structured` — blocks,\n regions, tables, figures, chunks, polygons, and bounding boxes. Pass the\n whole parse output (not just `.text`) into downstream `ai.extract` to enable\n shared grounded extraction, spatial citations, and lineage sidecars.\n- **You need strict data-egress control.** Use `parseMode: native` when embedded\n PDF text is enough and document bytes must not leave your deployment for OCR or\n vision. Use `parseMode: native-or-ocr` when most pages have good embedded text\n but some pages show objectively detectable extraction anomalies.\n- **You need column-aligned native PDF text.** Use `outputFormat: layout` with\n native extraction when markdown would flatten multi-column tables or forms and\n you want fixed-width plain text with horizontal gaps preserved.\n\n## How parsing is chosen\n\nThe step picks a strategy based on the document and your config:\n\n### Native (`parseMode: native`)\n\nLocal extraction only — embedded PDF text is read in the worker with **no OCR or\nvision egress**. There are **no parsing credits**. Raster images (PNG, JPG, TIFF,\nand similar) are **not supported** in this mode. **`describeFigures` is\nincompatible** — figure descriptions require a vision pass.\n\nNative extraction is **not a universal OCR replacement**. PDFs can render glyphs\nwithout usable Unicode mappings; diagnostics report detectable problems (empty\npages, replacement characters, forbidden controls, unassigned/noncharacter code\npoints, heavy private-use) but cannot certify that valid-looking text is\nsemantically correct. If **every** page is empty or suspect, the step **fails**\ninstead of returning unusable text or silently calling OCR. Mixed documents keep\nthe native text and attach per-page `nativeTextQuality`.\n\nOffice formats and plain text still use native extraction. Paginated word-processing\nand presentation formats use headless LibreOffice when per-page boundaries are\nneeded. Spreadsheets are read directly: each non-empty sheet becomes a named page,\nincluding legacy and macro-enabled Excel workbooks. Formulas and macros are never\nexecuted. Blank sheets are omitted and `pageIndex` counts the returned non-empty\nsheets, so use `pageName` when you need the workbook's sheet identity.\n\n### Spatial layout text (`outputFormat: layout`)\n\nReturns the same `text` and `pages[].text` fields as other formats, but as\nfixed-width plain text that preserves column spacing and line breaks from the\nPDF page layout. Use it for multi-column forms, tables, or address blocks where\n`markdown` (the default) would collapse horizontal alignment.\n\nConstraints:\n\n- **PDF only** — Office files, images, and plaintext fail. There is no\n Kreuzberg or OCR substitute for those types.\n- **Explicit native parse mode** — set `parseMode: native` or\n `parseMode: native-or-ocr`. Omitted `parseMode`, OCR, vision, and\n `nativeText: true` reject `layout`.\n- **Native mode is fail-closed** — every page is spatial layout text. If\n extraction fails, the step fails.\n- **Hybrid output can mix formats** — `native-or-ocr` keeps layout text on\n accepted native pages and uses the OCR provider's markdown on suspect or\n empty pages.\n- **Extractor failure is fail-closed** — a Poppler process error never\n silently routes to Kreuzberg or OCR.\n- **Other formats unchanged** — `markdown`, `plain`, `djot`, and `html` behave\n as before; `markdown` remains the default.\n\nLocal development requires `pdftotext` on `PATH` (`brew install poppler` on\nmacOS, `poppler-utils` on Linux). Managed deployments include this dependency.\n\n```yaml\n- name: parse\n type: ai.parse\n with:\n input: '{{ input.document }}'\n parseMode: native\n outputFormat: layout\n```\n\n### Native with OCR fallback (`parseMode: native-or-ocr`)\n\nStarts with the same local native extraction, then runs **page-quality diagnostics**\non each PDF page. Pages with **objectively detectable anomalies** — empty text,\nU+FFFD replacement characters, lone UTF-16 surrogates, forbidden controls,\nunassigned/noncharacter code points, or heavy private-use areas — trigger the\nconfigured OCR engine.\nValid-looking wrong mappings and literal `?` characters are **not** flagged; native\ndiagnostics report detectable issues but do **not** certify semantic correctness.\n\nWhen the OCR provider supports page selection, only fallback pages are processed\nand billed. Page selection does not imply reduced-byte egress: a provider may\nstill receive the original document bytes. When page selection is unavailable,\nthe provider processes and bills the **whole document**. Only detected fallback\npages replace native pages in the merged output; clean native pages are retained.\nWith `outputFormat: layout`, retained native pages stay as fixed-width layout\ntext and replaced pages use OCR markdown.\n\nYou pay **OCR credits for pages actually processed by the provider** — see\n`output.usage.ocrPagesProcessed` (which may reflect a whole-document OCR call even\nwhen only some pages replace native text). Per-page provenance is recorded on each\npage (`pages[].provenance`).\n\n### OCR (`parseMode: ocr`, the default)\n\nRuns optical character recognition for scanned PDFs and images. Select the engine\nwith `ocrModel`. Document bytes are sent to the OCR provider — **billed per\npage** according to the provider's credit rate.\n\n### Vision AI (`parseMode: vision`)\n\nReads rendered page images with the selected `llmModel`. Page images are sent to\nthe vision provider — **billed per page**. Use it for layouts where semantic\nvisual understanding matters. `pagesPerBatch` and `maxConcurrency` tune how pages\nare batched.\n\n### Legacy document-wide native text (`nativeText: true`)\n\nWhen set on **OCR or Vision** configs, the worker tries embedded PDF text first\nand falls back to the selected parser for the **whole document** if native text\nis missing. This is legacy behavior — prefer explicit `parseMode: native` or\n`native-or-ocr` for predictable egress and billing.\n\n### Figure descriptions (`describeFigures: true`)\n\nOptional vision pass **after** text extraction. With OCR or hybrid native+OCR,\n`figureModel` chooses the caption model; with Vision AI, the selected `llmModel`\nis reused. **Unavailable in Native mode.** The layout scan and caption calls are\nbilled separately and send page images to the vision provider.\n\nPaginated Office formats are converted to PDF first (via headless LibreOffice) so\nthey get true per-page boundaries instead of a single blob. Spreadsheets stay\nworkbook-native to avoid cropping wide sheets.\n\n## Data egress and cost\n\n| Mode | Document egress | Typical cost |\n| --- | --- | --- |\n| `native` | None for parsing | No parsing credits |\n| `native-or-ocr` | Capability-dependent: the provider may receive the full document even when it processes selected pages; without page selection it receives/processes the whole document | OCR credits × `usage.ocrPagesProcessed` (provider pages dispatched) |\n| `ocr` | OCR provider receives document/pages | OCR credits × page count |\n| `vision` | Vision provider receives rendered page images | Vision credits × page count |\n| `describeFigures` | Vision provider receives figure pages (additive) | Vision credits for captioned pages |\n\nThere is **one combined text output** — the worker does not emit separate native\nand OCR text streams.\n\n## Example\n\n```yaml\n- name: parse\n type: ai.parse\n with:\n input: '{{ input.document }}'\n parseMode: native-or-ocr\n ocrModel: openparser-default\n```\n\nFor a digital PDF with one page of garbled embedded text, most pages stay native\nin the merged output. OpenParser currently processes the whole document for OCR,\nso every page incurs OCR processing and credits. Azure can process and bill only\nthe anomalous page, but the current Azure adapter still uploads the original PDF\nbytes; page selection is not a subset-egress guarantee.\n\n## Content cache (`cache: true`)\n\nOpt-in reuse of prior parse output for **identical file bytes, filename, MIME type, and parse settings**.\nWhen enabled, the worker computes `SHA-256(file bytes + normalized parse config + filename + mimeType)`,\nlooks up a JSON blob in tenant storage (`_meta/parse-cache/<digest>.json`), and on\nhit **skips OCR/vision entirely** — no OpenParser call, no parse credits for that\nstep. On miss it parses normally and stores the result under the same key.\nResults are not cached when an optional `describeFigures` pass fails.\n\n- **Default:** `false` (safe for multi-tenant workflows; turn on when repeat parsing\n of the same uploads is expected, e.g. corpus preprocessing).\n- **Expiry:** none for now — entries persist until manually deleted or a future\n lifecycle policy lands.\n- **Scope:** tenant-isolated blob storage via the existing S3/R2 adapter (`_meta/parse-cache/<digest>.json`).\n- **Usage on hit:** returns the cached parse text/pages, but `usage.processingTimeMs` and `usage.ocrPagesProcessed` are **zero** and `usage.cached` is **true** so run metrics and OCR page counts are not double-counted. Fresh parses still record normal usage.\n\n```yaml\n- name: parse\n type: ai.parse\n with:\n input: '{{ input.document }}'\n parseMode: ocr\n ocrModel: openparser/paddleocr-vl-1.6\n cache: true\n```\n\n## Configuration\n\nConfiguration goes inside the step's `with:` block.\n\n{/* GENERATED:CONFIG START */}\n<ParamField path=\"input\" type=\"string\" required>\nStorage reference or template expression for the document\n</ParamField>\n<ParamField path=\"parseMode\" type=\""ocr" | "vision" | "native" | "native-or-ocr"\">\nBase parser for PDF/image inputs. OCR is the default; vision uses the selected LLM. `native` extracts PDF embedded text only and never calls OCR or vision. `native-or-ocr` extracts native text, runs page-quality diagnostics, and requests OCR when pages have detectable anomalies (empty, U+FFFD, lone surrogates, forbidden controls, unassigned/noncharacter, heavy PUA). Page selection, subset egress, and billing are provider-dependent. Valid-looking wrong text and literal \"?\" are not flagged. Text and Office files always use local parsing.\n</ParamField>\n<ParamField path=\"ocrModel\" type=\"string\">\nOCR provider ID for PDF/image parsing\n</ParamField>\n<ParamField path=\"llmModel\" type=\"string\">\nLLM provider ID for vision-based parsing\n</ParamField>\n<ParamField path=\"llmReasoningEffort\" type=\""none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"\">\nReasoning effort for models that support it. Omit to use the selected model's default.\n</ParamField>\n<ParamField path=\"figureModel\" type=\"string\">\nVision model used only for the optional figure-description pass\n</ParamField>\n<ParamField path=\"figureReasoningEffort\" type=\""none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"\">\nReasoning effort for models that support it. Omit to use the selected model's default.\n</ParamField>\n<ParamField path=\"maxConcurrency\" type=\"number\" default=\"3\">\nMax concurrent VLM batch requests\n</ParamField>\n<ParamField path=\"pagesPerBatch\" type=\"number\" default=\"5\">\nNumber of page images per VLM request\n</ParamField>\n<ParamField path=\"pdfRenderScale\" type=\"number\" default=\"1\">\nScale factor for rendering PDF pages before VLM parsing. Higher values produce sharper images at larger payload sizes.\n</ParamField>\n<ParamField path=\"imageQuality\" type=\"integer\" default=\"85\">\nJPEG quality for rendered PDF page images sent to VLM parsing. Higher values reduce compression artifacts at larger payload sizes.\n</ParamField>\n<ParamField path=\"prompt\" type=\"string\">\nCustom extraction prompt\n</ParamField>\n<ParamField path=\"languages\" type=\"array<string>\">\nOCR language hints\n</ParamField>\n<ParamField path=\"outputFormat\" type=\""plain" | "markdown" | "djot" | "html" | "layout"\" default=\"markdown\">\nFormat for extracted text. `markdown` (default) keeps structure; `plain` is unstyled text; `djot`/`html` preserve more markup. `layout` is native-PDF spatial text with column gaps preserved as spaces. It requires `parseMode: native` or `parseMode: native-or-ocr` and a PDF, omitted parseMode and `nativeText: true` are not enough. Native mode is layout-only and fail-closed. `native-or-ocr` keeps layout text on accepted native pages and uses OCR markdown on suspect or empty pages, so the document can mix fixed-width layout and markdown. Extractor failure never falls back to another parser. Office, plaintext, and images fail. OCR/vision cannot request `layout`.\n</ParamField>\n<ParamField path=\"nativeText\" type=\"boolean\" default=\"false\">\nExtract native/embedded text from PDFs without OCR/VLM. Faster and uses no credits. Falls back to OCR/VLM if the PDF has no embedded text.\n</ParamField>\n<ParamField path=\"describeFigures\" type=\"boolean\">\nOpt-in (default off). After text extraction, detect which pages contain figures with an in-worker layout model, then caption those pages with a vision model and append `<figure>description</figure>` to their text, so image-only pages (property photos, signatures, charts) become findable by text-based steps like ai.split. Note: the layout scan runs over all pages, and the caption step and its vision calls are billed. Skipped for plaintext.\n</ParamField>\n<ParamField path=\"figureInstructions\" type=\"string\">\nCustom instruction for the figure-description pass, e.g. \"Describe each figure; label a handwritten signature as `<figure>signature</figure>` and a stamp as `<figure>stamp</figure>`; for property photos note the room or exterior shown.\" Applied only when describeFigures runs.\n</ParamField>\n<ParamField path=\"cache\" type=\"boolean\" default=\"false\">\nWhen true, reuse a prior parse for identical file bytes and parse settings. Skips OCR/vision on cache hit. Stored in tenant blob storage with no automatic expiry (TTL/lifecycle is a follow-up). Default off.\n</ParamField>\n{/* GENERATED:CONFIG END */}\n\n## Output\n\nAfter a run, inspect **`processingStrategy`** for the resolved path: `native`,\n`ocr`, `vision`, or `hybrid` (native pages merged with OCR on fallback pages).\nEach page may include **`provenance`** (`native` or `ocr`) and\n**`nativeTextQuality`** when native diagnostics ran — diagnostics report\ndetectable anomalies only, not semantic correctness.\n**`usage.ocrPagesProcessed`** counts pages billed through the OCR provider on\nhybrid runs.\n\n**`structured`** carries an OpenParser `ParsedDocument` (`openparser@1`) when\nthe parser produced one. Downstream `ai.extract` validates it at runtime; invalid\nor absent structured data keeps text-only extraction and text grounding.\n\nPer-page provenance badges in Studio run detail are planned; until then use these\noutput fields in templates, CLI inspection, or the execution JSON view.\n\n{/* GENERATED:OUTPUT START */}\n<ResponseField path=\"document\" type=\"object\" required>\n<Expandable title=\"document properties\">\n<ResponseField path=\"filename\" type=\"string\" required>\n</ResponseField>\n<ResponseField path=\"mimeType\" type=\"string\" required>\n</ResponseField>\n<ResponseField path=\"size\" type=\"number\" required>\nFile size in bytes\n</ResponseField>\n<ResponseField path=\"storageRef\" type=\"string\">\nStorage reference for the original file\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"usage\" type=\"object\" required>\n<Expandable title=\"usage properties\">\n<ResponseField path=\"pageCount\" type=\"number\" required>\n</ResponseField>\n<ResponseField path=\"processingTimeMs\" type=\"number\">\n</ResponseField>\n<ResponseField path=\"ocrPagesProcessed\" type=\"integer\">\n</ResponseField>\n<ResponseField path=\"cached\" type=\"boolean\">\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"pages\" type=\"array<object>\" required>\n<Expandable title=\"pages properties\">\n<ResponseField path=\"pageIndex\" type=\"number\" required>\n0-based page index\n</ResponseField>\n<ResponseField path=\"text\" type=\"string\" required>\nExtracted page text (markdown/HTML/plain, or spatial layout text)\n</ResponseField>\n<ResponseField path=\"pageName\" type=\"string\">\nPage/sheet name (e.g., Excel sheet name)\n</ResponseField>\n<ResponseField path=\"layoutElements\" type=\"array<object>\">\nSemantic layout elements in reading order\n<Expandable title=\"layoutElements properties\">\n<ResponseField path=\"type\" type=\""paragraph" | "title" | "sectionHeading" | "pageHeader" | "pageFooter" | "pageNumber" | "footnote" | "table" | "figure" | "formulaBlock"\" required>\n</ResponseField>\n<ResponseField path=\"content\" type=\"string\" required>\n</ResponseField>\n<ResponseField path=\"table\" type=\"object\">\n</ResponseField>\n<ResponseField path=\"caption\" type=\"string\">\n</ResponseField>\n<ResponseField path=\"figureId\" type=\"string\">\n</ResponseField>\n<ResponseField path=\"boundingRegion\" type=\"object\">\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"words\" type=\"array<object>\">\nWord-level positions\n<Expandable title=\"words properties\">\n<ResponseField path=\"text\" type=\"string\" required>\n</ResponseField>\n<ResponseField path=\"confidence\" type=\"number\">\n</ResponseField>\n<ResponseField path=\"boundingRegion\" type=\"object\">\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"lines\" type=\"array<object>\">\nLine-level positions\n<Expandable title=\"lines properties\">\n<ResponseField path=\"text\" type=\"string\" required>\n</ResponseField>\n<ResponseField path=\"confidence\" type=\"number\">\n</ResponseField>\n<ResponseField path=\"boundingRegion\" type=\"object\">\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"tables\" type=\"array<object>\">\nExtracted tables\n<Expandable title=\"tables properties\">\n<ResponseField path=\"cells\" type=\"array<object>\" required>\n</ResponseField>\n<ResponseField path=\"rowCount\" type=\"number\" required>\n</ResponseField>\n<ResponseField path=\"columnCount\" type=\"number\" required>\n</ResponseField>\n<ResponseField path=\"boundingRegion\" type=\"object\">\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"width\" type=\"number\">\nPage width\n</ResponseField>\n<ResponseField path=\"height\" type=\"number\">\nPage height\n</ResponseField>\n<ResponseField path=\"unit\" type=\""pixel" | "inch" | "point" | "normalized"\">\nUnit for width/height and bounding regions\n</ResponseField>\n<ResponseField path=\"confidence\" type=\"number\">\nOverall page confidence\n</ResponseField>\n<ResponseField path=\"nativeTextQuality\" type=\"object\">\nPer-page diagnosis of native-extracted text. Reports objectively detectable anomalies (empty, U+FFFD, forbidden controls, unassigned/noncharacter code points, heavy private-use). Does not certify semantic correctness, valid-looking wrong mappings and literal \"?\" cannot be distinguished from legitimate text.\n<Expandable title=\"nativeTextQuality properties\">\n<ResponseField path=\"status\" type=\""clean" | "empty" | "suspect"\" required>\n</ResponseField>\n<ResponseField path=\"reasons\" type=\"array<"empty" | "replacement-character" | "lone-surrogate" | "forbidden-control" | "unassigned" | "noncharacter" | "private-use">\" required>\n</ResponseField>\n<ResponseField path=\"counts\" type=\"object\" required>\n</ResponseField>\n<ResponseField path=\"profile\" type=\"object\" required>\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"provenance\" type=\"object\">\nWhich extractor produced the final text/layout on this page\n<Expandable title=\"provenance properties\">\n<ResponseField path=\"source\" type=\""native" | "ocr" | "vision"\" required>\n</ResponseField>\n</Expandable>\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"text\" type=\"string\" required>\nCombined text from all pages\n</ResponseField>\n<ResponseField path=\"parserType\" type=\""plaintext" | "office" | "llm-vision" | "ocr"\" required>\n</ResponseField>\n<ResponseField path=\"parserVersion\" type=\"string\">\n</ResponseField>\n<ResponseField path=\"model\" type=\"string\">\nModel used (for LLM/OCR parsers)\n</ResponseField>\n<ResponseField path=\"processingStrategy\" type=\""native" | "ocr" | "vision" | "hybrid"\">\nHow this result was produced: `native` (local PDF text only), `ocr`, `vision`, or `hybrid` (native pages merged with OCR on fallback pages).\n</ResponseField>\n<ResponseField path=\"structured\" type=\"record<string, unknown>\">\nCanonical structured document with ordered blocks, regions, bounding boxes, tables, figures, and chunks when supported by the parser\n</ResponseField>\n{/* GENERATED:OUTPUT END */}\n"
|
|
1389
1403
|
},
|
|
1390
1404
|
{
|
|
1391
1405
|
"topic": "steps/ai/search-files",
|
|
@@ -1492,6 +1506,13 @@
|
|
|
1492
1506
|
"source": "public",
|
|
1493
1507
|
"content": "---\ntitle: \"Combine Data\"\ndescription: \"Merge multiple objects or concatenate arrays\"\n---\n\n`transform.combine`, Merge multiple objects or concatenate arrays\n\n{/* Write hand-authored prose here: when to reach for this step, gotchas,\n a worked example. Everything outside the GENERATED fences is preserved. */}\n\n## Configuration\n\nConfiguration goes inside the step's `with:` block.\n\n{/* GENERATED:CONFIG START */}\n<ParamField path=\"sources\" type=\"array<string>\" required>\nTemplate expressions for sources to combine\n</ParamField>\n<ParamField path=\"target\" type=\"string\" required>\nTarget path in output\n</ParamField>\n<ParamField path=\"mode\" type=\""merge" | "concat" | "deep"\" default=\"merge\">\n</ParamField>\n{/* GENERATED:CONFIG END */}\n\n## Output\n\n{/* GENERATED:OUTPUT START */}\nReturns `record<string, unknown> | array<unknown>`. Combined result\n{/* GENERATED:OUTPUT END */}\n"
|
|
1494
1508
|
},
|
|
1509
|
+
{
|
|
1510
|
+
"topic": "steps/transform/crop-regions",
|
|
1511
|
+
"title": "Crop Regions",
|
|
1512
|
+
"description": "Render a PDF or image and crop normalized bounding boxes into JPEG run output artifacts.",
|
|
1513
|
+
"source": "public",
|
|
1514
|
+
"content": "---\ntitle: \"Crop Regions\"\ndescription: \"Render a PDF or image and crop normalized bounding boxes into JPEG run output artifacts.\"\n---\n\n`transform.crop-regions` rasterizes a PDF or image, crops each normalized\nbounding box you supply, and stores every crop as a JPEG run output file. Use it\nwhen downstream steps need image artifacts — figure chips, table snippets, or\nany layout-driven region — without baking figure semantics into the step itself.\n\nRegions are plain data: `{ id?, pageIndex, bbox }` with `bbox` as normalized\n`[x0, y0, x1, y1]` on the rendered page (origin top-left). Build the list with\n[`transform.script`](/steps/transform/script) from [`ai.parse`](/steps/ai/parse)\nlayout output, or pass any workflow-authored array.\n\nTypical preprocessing chain for parsed figures:\n\n```yaml\nsteps:\n - name: parse\n type: ai.parse\n with:\n input: \"{{ input.document }}\"\n parseMode: ocr\n\n - name: list-figures\n type: transform.script\n with:\n inputs:\n parsed: \"{{ steps.parse.output }}\"\n function: |\n function script(parsed: { pages: { pageIndex: number; width?: number; height?: number; layoutElements?: { type?: string; figureId?: string; boundingRegion?: { polygon?: { x?: number; y?: number }[]; unit?: string; pageIndex?: number } } }[] }[] }): { regions: { id: string; pageIndex: number; bbox: [number, number, number, number] }[] } {\n function normalizeBbox(\n xs: number[],\n ys: number[],\n unit: string | undefined,\n pageWidth: number,\n pageHeight: number\n ): [number, number, number, number] | null {\n if (!xs.length || pageWidth <= 0 || pageHeight <= 0) return null;\n const x0 = Math.min(...xs);\n const y0 = Math.min(...ys);\n const x1 = Math.max(...xs);\n const y1 = Math.max(...ys);\n if (unit === \"normalized\" || unit === undefined) {\n return [x0, y0, x1, y1];\n }\n return [x0 / pageWidth, y0 / pageHeight, x1 / pageWidth, y1 / pageHeight];\n }\n\n const regions: { id: string; pageIndex: number; bbox: [number, number, number, number] }[] = [];\n for (const page of parsed.pages ?? []) {\n const pageWidth = page.width ?? 0;\n const pageHeight = page.height ?? 0;\n for (const el of page.layoutElements ?? []) {\n if (el.type !== \"figure\") continue;\n const poly = el.boundingRegion?.polygon ?? [];\n const xs = poly.map((p) => p.x ?? 0);\n const ys = poly.map((p) => p.y ?? 0);\n const bbox = normalizeBbox(\n xs,\n ys,\n el.boundingRegion?.unit,\n pageWidth,\n pageHeight\n );\n if (!bbox) continue;\n regions.push({\n id: el.figureId ?? `fig${regions.length + 1}`,\n pageIndex: el.boundingRegion?.pageIndex ?? page.pageIndex,\n bbox,\n });\n }\n }\n return { regions };\n }\n\n - name: crop-figures\n type: transform.crop-regions\n with:\n input: \"{{ input.document }}\"\n regions: \"{{ steps.list-figures.output.regions }}\"\n```\n\nEach crop in `steps.crop-figures.output.regions[]` includes `fileId`, `filename`,\nand pixel dimensions. Caption crops with [`ai.extract`](/steps/ai/extract) inside\n`control.parallel_map`, or inspect full pages with [`ai.vision`](/steps/ai/vision).\n\n## Configuration\n\nConfiguration goes inside the step's `with:` block.\n\n{/* GENERATED:CONFIG START */}\n<ParamField path=\"input\" type=\"string\" required>\nFile input, template expression e.g. {{ input.document }} resolving to a PDF or image\n</ParamField>\n<ParamField path=\"regions\" type=\"string | array<object>\" required>\nRegions to crop, pageIndex + normalized bbox per entry\n</ParamField>\n<ParamField path=\"renderScale\" type=\"number\" default=\"1\">\nScale factor when rasterizing PDF pages before cropping\n</ParamField>\n<ParamField path=\"imageQuality\" type=\"integer\" default=\"85\">\nJPEG quality for cropped outputs\n</ParamField>\n<ParamField path=\"paddingFrac\" type=\"number\" default=\"0.02\">\nPadding around each bbox as a fraction of the shorter page edge\n</ParamField>\n<ParamField path=\"minCropPx\" type=\"integer\" default=\"8\">\nMinimum crop width/height in pixels; smaller crops fall back to the full page\n</ParamField>\n<ParamField path=\"maxRegions\" type=\"integer\" default=\"100\">\nMaximum regions processed per invocation\n</ParamField>\n{/* GENERATED:CONFIG END */}\n\n## Output\n\n{/* GENERATED:OUTPUT START */}\n<ResponseField path=\"regions\" type=\"array<object>\" required>\nSuccessfully cropped regions\n<Expandable title=\"regions properties\">\n<ResponseField path=\"id\" type=\"string\" required>\nRegion id (from input or auto-generated)\n</ResponseField>\n<ResponseField path=\"pageIndex\" type=\"integer\" required>\n0-based page index the crop was taken from\n</ResponseField>\n<ResponseField path=\"bbox\" type=\"array<number>\" required>\nNormalized bbox that was cropped\n</ResponseField>\n<ResponseField path=\"width\" type=\"integer\" required>\nCrop width in pixels\n</ResponseField>\n<ResponseField path=\"height\" type=\"integer\" required>\nCrop height in pixels\n</ResponseField>\n<ResponseField path=\"fileId\" type=\"string\" required>\nRun output file id for the JPEG crop\n</ResponseField>\n<ResponseField path=\"filename\" type=\"string\" required>\nStored crop filename\n</ResponseField>\n<ResponseField path=\"mimeType\" type=\"string\" required>\nAlways image/jpeg\n</ResponseField>\n<ResponseField path=\"size\" type=\"integer\" required>\nJPEG size in bytes\n</ResponseField>\n</Expandable>\n</ResponseField>\n<ResponseField path=\"skipped\" type=\"integer\">\nRegions skipped (invalid bbox or missing page)\n</ResponseField>\n{/* GENERATED:OUTPUT END */}\n"
|
|
1515
|
+
},
|
|
1495
1516
|
{
|
|
1496
1517
|
"topic": "steps/transform/json-to-xlsx",
|
|
1497
1518
|
"title": "JSON to XLSX",
|