Decision 03
One grammar for pipelines
A user recipe and a built-in template are the same YAML file, so the visual builder and the DSL stay a single object.
The decision
A recipe built by clicking through the UI and a recipe written by hand are the same YAML file. There is no separate, prettier user-facing format.
This matters because the alternative — a bespoke UI-only representation that gets translated into the internal template format — is exactly how the orchestrator ended up with two YAML dialects (see The engine already exists). One grammar means the "save as recipe" button in the builder produces something that could ship as a built-in template, and a built-in template can be opened and forked by a user.
What already parses
This is a real, shipping pipeline definition — not a mockup.
# Workflow/Template/zip-office-to-zip-pdf.yaml — real, shipping
taskType: "zip-office-to-zip-pdf"
description: "Convert a zip of office documents to a zip of PDFs"
settings:
maxPagesPerPart: 1
supportsSplitting: false
requiresMerging: false
steps:
- name: "extract_archive"
pluginName: "archive-extract"
dependsOn: []
priority: 0
- name: "convert_to_pdf"
pluginName: "soffice-convert"
dependsOn: ["extract_archive"]
priority: 100
- name: "create_archive"
pluginName: "archive-create"
dependsOn: ["convert_to_pdf"]
priority: 200Named steps, a dependency graph, parallelism and priority — that is already a real pipeline DSL, authored today only by us as a shipped template. Three additions make it authorable by a workspace.
The proposed recipe grammar
New fields only. Nothing existing changes shape.
# A workspace recipe. New fields marked with ← new.
version: 1 ← new — lets the grammar evolve
name: "Contracts to archived PDF/A"
inputs: ← new — named library files, not "the task's files"
from: library
match: "contracts/**/*.docx"
limit: 500
surface: serverless ← new — where it runs
settings:
maxPagesPerPart: 100
supportsSplitting: true
requiresMerging: true
steps:
- name: to_pdf
pluginName: soffice-convert
with: ← new — named, validated options
output-format: pdf
dependsOn: []
- name: archive_quality
pluginName: pdf-saveas
with: { format: pdfa2b }
dependsOn: [to_pdf]
- name: stamp
pluginName: esign
with: { reason: "Archived copy" }
dependsOn: [archive_quality]| Addition | Why it's needed | Cost |
|---|---|---|
| version | The grammar will change over time; nothing on disk today carries a version marker. | Trivial |
| inputs | Today the input set is implicit — whatever the task happened to carry. A recipe must name its own inputs to be re-runnable on demand. | Needs a workspace-scoped file library |
| with | A step currently receives the same task payload as every other step in its pipeline, with only the id rewritten — there is no way to give two steps different settings. | Carry per-step options through the plan builder |
| surface | Which execution surface a run is submitted to. | Small — a queue selector |
One schema, three surfaces
The mechanic that keeps the DSL, the web form and the agent tool arguments from drifting apart.
Every plugin already declares its parameters as a small, typed options class — that declaration is the single source for the plugin's command-line flags, its JSON binding and its generated help text. A build-time pass over every plugin's options type can publish that declaration as one document, listing each plugin's cost, concurrency and parameters together.
| Consumer | What it gets |
|---|---|
| Run builder UI | Generated form fields with labels, help text, defaults and validation — no per-plugin front-end code. |
| Recipe YAML | Validation of every with: block against the same schema. |
| MCP tools | Argument descriptions generated from the same source instead of written by hand per tool. |
| This documentation | The plugin reference table, generated rather than maintained. |
Without a shared schema, adding a plugin means separately updating a form, a YAML validator, an MCP tool signature and a documentation page. With one, adding a plugin means adding a plugin.