Pipeline IR
Pipeline IR
Section titled “Pipeline IR”ado-aw no longer compiles pipelines by substituting strings into YAML template files. Every production target builds a typed Azure DevOps pipeline IR, resolves graph-level facts, lowers that IR to serde_yaml::Value, and serializes once with serde_yaml::to_string.
The implementation lives under src/compile/ir/. The canonical 5-job agentic-pipeline shape (Setup → Agent → Detection → SafeOutputs → Teardown) lives in src/compile/agentic_pipeline.rs and is shared by every target. Per-target wrappers handle only the envelope:
src/compile/standalone_ir.rssrc/compile/onees_ir.rssrc/compile/job_ir.rssrc/compile/stage_ir.rs
Those wrappers are the only place per-target shape (top-level PipelineShape, template parameters, 1ES extends:) should be assembled. Shared canonical-shape logic belongs in agentic_pipeline.rs. Shared target logic should be typed IR construction helpers, not string fragments.
Module layout
Section titled “Module layout”src/compile/ir/ is split by responsibility:
ids.rs— typedStageId,JobId, andStepIdnewtypes. Constructors validate the ADO identifier grammar (^[A-Za-z_][A-Za-z0-9_]*$) so invalid names fail at compile time.step.rs—Stepand concrete step structs:BashStep,TaskStep,CheckoutStep,DownloadStep, andPublishStep.tasks/— typed builder structs for 44 built-in ADO task steps spanning toolchain setup, file operations, build/test, artifact publishing, scripting, containers, package auth, Azure integrations, and pipeline control. Each builder exposesnew(<required>), typed optional setters, andinto_step(). Prefer these over hand-craftedTaskStep::new()calls — see Typed task helpers below.job.rs—Job,Pool, job variables, 1EStemplateContextsupport, and target-job externaldependsOn/conditionwrapping.stage.rs—Stageplus target-stage externaldependsOn/conditionwrapping.env.rs— typed environment values (EnvValue) including ADO macros, pipeline variables, secrets,OutputRefs,Coalesce, and macro-formConcat.condition.rs— theCondition/ExprAST and code generation to ADO condition syntax.output.rs—OutputDecl,OutputRef, and the output-reference lowering rules.graph.rs— graph construction,dependsOnderivation, output validation,isOutput=truepromotion, and cycle detection.validatepass — there is no separatevalidate.rsmodule in the current tree; graph invariants live ingraph.rs, shape checks live near the relevant lowering code inlower.rs, and target-specific validation stays in the target builder.lower.rs— converts typed IR to aserde_yaml::Valuetree.emit.rs— callslower::lower()andserde_yaml::to_string()for canonical YAML output.
Top-level pipeline types
Section titled “Top-level pipeline types”The root type is Pipeline in src/compile/ir/mod.rs:
pub struct Pipeline { pub name: String, pub parameters: Vec<Parameter>, pub resources: Resources, pub triggers: Triggers, pub variables: Vec<PipelineVar>, pub body: PipelineBody, pub shape: PipelineShape,}PipelineBody captures whether the emitted document has a top-level jobs: block or a top-level stages: block:
pub enum PipelineBody { Jobs(Vec<Job>), Stages(Vec<Stage>),}PipelineShape captures the wrapping rules that used to be split across template files:
pub enum PipelineShape { Standalone, OneEs { sdl, top_level_pool, stage_id, stage_display_name }, JobTemplate { external_params }, StageTemplate { external_params },}Shape is intentionally separate from body. For example, the 1ES target still builds the canonical job graph as PipelineBody::Jobs; the lowering pass wraps those jobs under the 1ES extends.parameters.stages[0].jobs shape.
All generated pipeline steps should use typed variants from src/compile/ir/step.rs:
pub enum Step { Bash(BashStep), Task(TaskStep), Checkout(CheckoutStep), Download(DownloadStep), Publish(PublishStep), RawYaml(String),}Use the typed structs whenever the compiler owns the step:
Step::Bashfor inline bash (BashStep::scriptis the raw body, not a YAML block).Step::Taskfor ADO task invocations such asUseNode@1,UsePythonVersion@0, orUseDotNet@2.Step::Checkoutforcheckout:steps.Step::Downloadfor pipeline-artifact downloads.Step::Publishfor pipeline-artifact publishes. Under 1ES, lowering moves publish steps intotemplateContext.outputsso artifacts are published by the 1ES template machinery exactly once.Step::RawYamlis reserved for user-authored setup/teardown YAML that the IR does not model. Do not use it for compiler-generated steps that need output refs, conditions, env rewriting, or graph-derived dependencies.
BashStep and TaskStep carry common compiler-owned fields:
id: Option<StepId>— emitted as ADO stepname:; required when another step consumes an output from this step.display_name: String— emitted asdisplayName:.env: IndexMap<String, EnvValue>— typed environment values.condition: Option<Condition>— typed ADO condition AST.timeout: Option<Duration>andcontinue_on_error: bool.outputs: Vec<OutputDecl>onBashStep.
Example:
let synth = Step::Bash( BashStep::new("Resolve synthetic PR", script) .with_id(StepId::new("synthPr")?) .with_output(OutputDecl::new("AW_SYNTHETIC_PR_ID")) .with_env("BUILD_REASON", EnvValue::ado_macro("Build.Reason")?),);Typed task helpers
Section titled “Typed task helpers”src/compile/ir/tasks/ contains typed builder structs for 44 ADO built-in tasks. Each follows the same pattern:
Builder::new(required_params) // required inputs as typed positional args .optional_setter(value) // chained typed setters — only emitted when called .into_step() // returns a TaskStepThis eliminates hand-crafted TaskStep::new(…) + raw string inputs at every call site, makes the required/optional boundary explicit, and prevents invalid input combinations for command-dispatch tasks at compile time.
Command-dispatch tasks (Docker@2, DotNetCoreCLI@2, NuGetCommand@2, Npm@1, PowerShell@2, AzurePowerShell@5, AzureCLI@2, PythonScript@0, VSTest@2, GitHubRelease@1, PublishBuildArtifacts@1, UniversalPackages@1) take a typed command/mode enum rather than a plain string — applying an input to the wrong command variant is unrepresentable.
Available builders
Section titled “Available builders”Toolchain setup
Section titled “Toolchain setup”| Builder | ADO task | Constructor |
|---|---|---|
DockerInstaller::new(docker_version) | DockerInstaller@0 | Docker engine version (e.g. "26.1.4") |
GoTool::new(version) | GoTool@0 | Go version (e.g. "1.21") |
JavaToolInstaller::new(version_spec, architecture, source) | JavaToolInstaller@0 | JDK version; JdkArchitecture enum; JdkSource enum |
UseDotNet::new() | UseDotNet@2 | All inputs optional — chain .with_version(s) or .with_global_json() |
UseNode::new(version) | UseNode@1 | Node.js version spec (e.g. "22.x") |
UsePythonVersion::new(version_spec) | UsePythonVersion@0 | Python version spec (e.g. "3.x") |
UseRubyVersion::new(version_spec) | UseRubyVersion@0 | Ruby version range (e.g. ">= 2.4") |
HelmInstaller::new() | HelmInstaller@1 | All inputs optional — chain .helm_version("3.x") to pin a specific version |
File operations
Section titled “File operations”| Builder | ADO task | Constructor |
|---|---|---|
ArchiveFiles::new(root_folder_or_file, archive_file) | ArchiveFiles@2 | source path; output archive path |
CopyFiles::new(contents, target_folder) | CopyFiles@2 | glob pattern; destination folder |
DeleteFiles::new(contents) | DeleteFiles@1 | newline-separated glob patterns |
DownloadPackage::new(package_type, feed, definition, version, download_path) | DownloadPackage@1 | PackageType enum; feed; definition; version; local path |
DownloadSecureFile::new(secure_file) | DownloadSecureFile@1 | secure-file name or GUID from ADO Secure Files library |
ExtractFiles::new(archive_file_patterns, destination_folder) | ExtractFiles@1 | archive glob; destination folder |
Script execution
Section titled “Script execution”| Builder | ADO task | Constructor |
|---|---|---|
AzureCli::new(azure_subscription, ScriptType, ScriptLocation) | AzureCLI@2 | ARM service connection; ScriptType::Bash | Ps | PsCore | Batch enum; ScriptLocation::Inline(script) | ScriptPath(path) enum |
AzurePowerShell::file(connection, script_path) / ::inline(connection, script) | AzurePowerShell@5 | Azure RM service connection; .ps1 path or inline script |
CmdLine::new(script) | CmdLine@2 | inline script text |
PowerShell::file(file_path) / ::inline(script) | PowerShell@2 | .ps1 path or inline script body |
PythonScript::file(script_path) / ::inline(script) | PythonScript@0 | .py path or inline script body |
Build and test
Section titled “Build and test”| Builder | ADO task | Constructor |
|---|---|---|
DotNetCoreCli::new(DotNetCommand) | DotNetCoreCLI@2 | DotNetCommand::Build | Test | Publish | Restore | Pack | Run | Push | Custom |
Gradle::new(gradle_wrapper_file, tasks) | Gradle@3 | path to gradlew; space-separated task names |
Maven::new(maven_pom_file) | Maven@3 | path to pom.xml |
PublishCodeCoverageResults::new(summary_file_location) | PublishCodeCoverageResults@2 | glob path to coverage summary file |
PublishTestResults::new(format, files) | PublishTestResults@2 | TestResultsFormat enum; result files glob |
VsTest::new(VsTestSelector) | VSTest@2 | VsTestSelector::Assemblies | Plan | Run |
VsBuild::new(solution) | VSBuild@1 | path to .sln or glob; VsVersion, MsBuildArchitecture, LogFileVerbosity enums and all other inputs optional |
Artifact publishing
Section titled “Artifact publishing”| Builder | ADO task | Constructor |
|---|---|---|
DownloadBuildArtifacts::new(download_path) | DownloadBuildArtifacts@1 | local download directory; BuildType, DownloadType, BuildVersionToDownload enums and all other inputs optional |
DownloadPipelineArtifact::new(target_path) | DownloadPipelineArtifact@2 | local download path |
PublishBuildArtifacts::new(path_to_publish, artifact_name, location) | PublishBuildArtifacts@1 | source path; artifact name; PublishLocation enum |
PublishPipelineArtifact::new(target_path) | PublishPipelineArtifact@1 | path to publish |
Containers and cloud
Section titled “Containers and cloud”| Builder | ADO task | Constructor |
|---|---|---|
AzureKeyVault::new(connected_service_name, key_vault_name) | AzureKeyVault@2 | Azure RM service connection; vault name |
AzureWebApp::new(azure_subscription, app_type, app_name, package) | AzureWebApp@1 | Azure RM service connection; AppType enum; app name; package path |
Docker::new(DockerCommand) | Docker@2 | DockerCommand::BuildAndPush | Build | Push | Login | Logout |
GitHubRelease::new(git_hub_connection, repository_name, action) | GitHubRelease@1 | GitHub service connection; "owner/repo"; GitHubReleaseAction enum |
Package authentication
Section titled “Package authentication”| Builder | ADO task | Constructor |
|---|---|---|
CargoAuthenticate::new(config_file) | CargoAuthenticate@0 | path to config.toml |
MavenAuthenticate::new() | MavenAuthenticate@0 | all inputs optional |
Npm::new(NpmCommand) | Npm@1 | NpmCommand::Install | Ci | Publish | Custom |
NpmAuthenticate::new(working_file) | npmAuthenticate@0 | path to .npmrc file to authenticate |
NuGetAuthenticate::new() | NuGetAuthenticate@1 | all inputs optional |
NuGetCommand::new(NuGetOp) | NuGetCommand@2 | NuGetOp::Restore | Push | Pack | Custom |
PipAuthenticate::new() | PipAuthenticate@1 | all inputs optional |
TwineAuthenticate::new() | TwineAuthenticate@1 | all inputs optional |
UniversalPackages::download(feed, package_name, spec) / ::publish(…, spec) | UniversalPackages@1 | UniversalPackagesDownload / UniversalPackagesPublish carry per-command optionals; .workload_identity_service_connection() for cross-org feeds |
Pipeline control
Section titled “Pipeline control”| Builder | ADO task | Constructor |
|---|---|---|
ManualValidation::new(notify_users) | ManualValidation@1 | comma-separated notification addresses; all approval settings (.approvers(), .instructions(), .on_timeout(OnTimeout::Reject | Resume)) optional |
Usage example
Section titled “Usage example”use crate::compile::ir::step::Step;use crate::compile::ir::tasks::{ dotnet_core_cli::{DotNetBuild, DotNetCoreCli, DotNetTest}, powershell::PowerShell, publish_test_results::{PublishTestResults, TestResultsFormat}, use_node::UseNode,};
// Install Node.js — simple builder, one required inputlet node = Step::Task(UseNode::new("22.x").into_step());
// Build .NET — command-dispatch with optional inputs on the command variantlet build = Step::Task( DotNetCoreCli::build( DotNetBuild::new() .projects("**/*.csproj") .arguments("--configuration Release"), ) .into_step(),);
// Run tests (separate command variant; cannot accidentally set build-only inputs)let test = Step::Task( DotNetCoreCli::test(DotNetTest::new().projects("**/*Tests.csproj")) .into_step(),);
// Publish test results — required inputs are typed, not raw stringslet publish = Step::Task( PublishTestResults::new(TestResultsFormat::VSTest, "**/*.trx").into_step(),);
// Inline PowerShell with optional pwsh flaglet ps = Step::Task( PowerShell::inline("Write-Output 'hello'") .pwsh(true) .into_step(),);Adding a new builder
Section titled “Adding a new builder”When you need an ADO task that doesn’t have a typed builder yet:
- Create
src/compile/ir/tasks/<snake_name>.rs. For a simple task, followuse_node.rsas the template. For a task with multiple commands or modes, followdocker.rs(canonical command-dispatch template). - Export the new module from
src/compile/ir/tasks/mod.rswithpub mod <snake_name>;. - Map required inputs to positional
pub fn new(…)parameters; optional inputs become chained setters that returnSelf. pub fn into_step(self) -> TaskStepemits only the inputs that areSome.- Add an ADO task reference link in the module doc comment.
- Write a unit test asserting the task identifier, display name, and that required inputs are present and optional inputs are absent when unset.
Do not use Step::RawYaml for tasks the IR can model. Typed builders preserve all compiler-owned fields (condition, env, timeout, continue_on_error) and participate correctly in the graph pass.
Output declarations and references
Section titled “Output declarations and references”A producer declares a step output with OutputDecl:
OutputDecl::new("AW_SYNTHETIC_PR_ID")OutputDecl::secret("MCP_GATEWAY_API_KEY")A consumer references it with OutputRef:
let r = OutputRef::new(StepId::new("synthPr")?, "AW_SYNTHETIC_PR_ID");EnvValue::step_output(r)The consumer does not choose the ADO expression syntax. output.rs::lower_outputref() chooses the correct syntax from the consumer and producer locations:
| Consumer vs. producer | Lowered syntax |
|---|---|
| Same job | $(stepName.X) |
| Sibling job in the same stage, or both jobs are stage-less | dependencies.<job>.outputs['stepName.X'] |
| Different stage | stageDependencies.<stage>.<job>.outputs['stepName.X'] |
This rule exists because Azure DevOps output variables are context-sensitive. The historical synthPr failures came from hand-written code using the wrong reference form for the consumer location. The IR centralizes that choice so new compiler code declares what it needs (OutputRef) rather than guessing how ADO will expose it.
graph.rs also sets OutputDecl::auto_is_output = true when any consumer reads the declaration. The producer can then emit ##vso[task.setvariable ...;isOutput=true] only when cross-step visibility is actually needed.
Graph pass
Section titled “Graph pass”graph.rs::resolve() is the all-in-one pass for dependency derivation:
- Index every named step and its declared outputs.
- Walk every
EnvValue::StepOutput, every output nested insideEnvValue::Coalesce/EnvValue::Concat, and everyExpr::StepOutputinside conditions. - Validate that each reference names an existing step with a matching
OutputDecl. - Lift step-output edges into job-level and stage-level dependencies.
- Detect cycles in the derived job and stage graphs.
- Merge the derived edges into
Job::depends_onandStage::depends_onwhile preserving any explicit values a target builder supplied. - Mark producer outputs that need
isOutput=true.
Same-job refs do not produce dependsOn entries because ADO orders steps by position. Cross-job refs add Job::depends_on; cross-stage refs add Stage::depends_on. The lowering pass reads those fields and emits canonical dependsOn: blocks.
Conditions
Section titled “Conditions”condition.rs defines a small AST for ADO conditions:
pub enum Condition { Succeeded, Always, Failed, SucceededOrFailed, And(Vec<Condition>), Or(Vec<Condition>), Not(Box<Condition>), Eq(Expr, Expr), Ne(Expr, Expr), Custom(String),}
pub enum Expr { Literal(String), Variable(String), StepOutput(OutputRef),}Use constructors such as Condition::and([...]), Condition::or([...]), and Condition::not(...) when composing nested expressions. Codegen flattens nested And / Or nodes and quotes string literals for ADO expression syntax:
Condition::Eq( Expr::Variable("Build.Reason".into()), Expr::Literal("PullRequest".into()),)lowers to:
eq(variables['Build.Reason'], 'PullRequest')Expr::StepOutput uses the same location-aware output-ref lowering as EnvValue::StepOutput. Condition::Custom is an escape hatch for expressions not yet modeled by the AST; codegen rejects embedded newlines and ADO pipeline-command markers (##vso[, ##[) before emitting it.
Extension declarations
Section titled “Extension declarations”The extension trait lives in src/compile/extensions/mod.rs and now has exactly three surface methods:
pub trait CompilerExtension { fn name(&self) -> &str; fn phase(&self) -> ExtensionPhase; fn declarations(&self, ctx: &CompileContext) -> Result<Declarations>;}Declarations is the typed aggregate for every signal an extension contributes:
agent_prepare_steps: Vec<Step>setup_steps: Vec<Step>agent_finalize_steps: Vec<Step>detection_prepare_steps: Vec<Step>safe_outputs_steps: Vec<Step>network_hosts: Vec<String>bash_commands: Vec<String>prompt_supplement: Option<String>mcpg_servers: Vec<(String, McpgServerConfig)>copilot_allow_tools: Vec<String>pipeline_env: Vec<PipelineEnvMapping>awf_mounts: Vec<AwfMount>awf_path_prepends: Vec<String>agent_env_vars: Vec<(String, String)>warnings: Vec<String>
Extension phases are System, Runtime, and Tool. The compiler sorts extensions by phase before merging declarations, so internal system plumbing lands first, runtime installs land before user tools, and tool extensions can assume requested runtimes are available.
Always-on extensions are collected in collect_extensions() before user-configured runtimes/tools:
AdoAwMarkerExtensionGitHubExtensionSafeOutputsExtensionAdoScriptExtensionExecContextExtensionAzureCliExtension
Lowering and emission
Section titled “Lowering and emission”lower.rs::lower() builds and validates a Graph, then converts the typed Pipeline into a serde_yaml::Value tree. The lowerer owns ADO wire shapes and canonical ordering: top-level identity and configuration keys first, then jobs: / stages:, with target-specific wrapping based on PipelineShape.
emit.rs::emit() is intentionally thin:
pub fn emit(pipeline: &Pipeline) -> Result<String> { let value = super::lower::lower(pipeline)?; serde_yaml::to_string(&value)}This gives all targets one serialization path and one canonical YAML style. Target compilers should return a complete typed Pipeline; they should not format YAML directly.
Per-target compilers
Section titled “Per-target compilers”The production target wrappers are:
standalone_ir.rs— wraps the canonical shape in a top-level standalone pipeline.onees_ir.rs— wraps the same canonical shape withPipelineShape::OneEs, causing the lowerer to emit the 1ESextends:wrapper andtemplateContextoutputs.job_ir.rs— wraps the canonical shape as a target-job template with externaldependsOn/conditiontemplate parameters.stage_ir.rs— wraps the canonical shape as a target-stage template with the stage-level external-parameter wrapper.
The canonical 5-job Setup → Agent → Detection → SafeOutputs → Teardown shape itself lives in agentic_pipeline.rs and is reused unchanged by every wrapper above; extensions plug into it via Declarations (steps, env, hosts, MCPG entries, and Agent-job condition clauses — see Declarations::agent_conditions).
When adding a target, follow the same pattern: parse and validate front matter, collect extension Declarations, build typed jobs/stages/steps, set the correct PipelineShape, and call the shared emit path.
Public JSON summary
Section titled “Public JSON summary”The internal IR types (Pipeline, Job, Step, Graph, …) are intentionally tied to the compiler’s lowering needs and are not public API. src/compile/ir/summary.rs defines a parallel summary tree with #[derive(Serialize)] that provides agent-facing tooling with a stable JSON view of a compiled pipeline.
This is the schema consumed by:
ado-aw inspect <source> --json— returns a fullPipelineSummaryado-aw graph dump <source> --format json— returns aGraphSummarysubsetado-aw graph deps/ado-aw graph outputs— focused graph queriesado-aw whatif— static downstream skip analysis built on graph reachabilityado-aw audit --json— thepipeline_graphfield inAuditData- The author MCP server tools (
inspect_workflow,graph_summary,graph_dump)
Stability contract
Section titled “Stability contract”PipelineSummary::schema_version (currently 1) is the public schema version. It is bumped when the JSON shape changes in a backwards-incompatible way — renamed field, removed variant, or changed semantics. Additive changes (new optional fields) do not require a bump. New enum variants do require a bump because the serialized enums have no catch-all Unknown variants.
Internal IR types may change freely without bumping the summary version, as long as the summary.rs lowering keeps the existing field set populated correctly.
Top-level shape
Section titled “Top-level shape”{ "schema_version": 1, "name": "<pipeline name>", // ADO build-number format string "shape": "standalone" | "1es" | "job-template" | "stage-template", "body": { "kind": "jobs", "jobs": [...] }, // OR { "kind": "stages", "stages": [...] }, "graph": { ... } // see GraphSummary below}The body discriminant (kind) mirrors PipelineBody: flat pipelines (standalone, job-template) use "jobs", stage-wrapped pipelines (1es, stage-template) use "stages".
JobSummary
Section titled “JobSummary”Each entry in body.jobs (or inside a stage’s jobs array):
| Field | Type | Description |
|---|---|---|
id | string | ADO job identifier (name: in YAML) |
stage | string or null | Stage id this job belongs to; null for flat (non-stage) pipelines |
display_name | string | Human-readable displayName: |
depends_on | string[] | dependsOn: entries — both explicitly declared and graph-derived |
condition | string or null | Lowered ADO condition expression, e.g. "succeeded()" |
pool | object | Pool summary — see PoolSummary below |
steps | object[] | Ordered list of step summaries — see StepSummary below |
PoolSummary has one of two shapes depending on pool type:
// Microsoft-hosted{ "kind": "vm_image", "image": "ubuntu-22.04" }
// Self-hosted or 1ES{ "kind": "named", "name": "MyPool", "image": null, "os": "linux" }StepSummary
Section titled “StepSummary”Each entry in job.steps:
| Field | Type | Description |
|---|---|---|
id | string or null | ADO step name: — present when other steps read this step’s outputs |
kind | string | Step variant: "bash", "task", "checkout", "download", "publish", "raw_yaml" |
display_name | string or null | Human-readable displayName: |
task | string or null | For task steps only: ADO task identifier, e.g. "UseNode@1" |
condition | string or null | Lowered ADO condition expression |
outputs | object[] | Output variables declared by this step |
env_refs | object[] | Other steps’ outputs read via this step’s env: block |
condition_refs | object[] | Other steps’ outputs read via this step’s condition: |
Each entry in outputs:
{ "name": "AW_SYNTHETIC_PR_ID", // ##vso[task.setvariable variable=...] name "is_secret": false, // true → value is masked in logs "auto_is_output": true // true → at least one cross-step consumer → emit isOutput=true}Each entry in env_refs / condition_refs:
{ "step": "synthPr", "name": "AW_SYNTHETIC_PR_ID" }GraphSummary
Section titled “GraphSummary”The graph field is a JSON-friendly view of the typed Graph built during lowering:
| Field | Type | Description |
|---|---|---|
step_locations | object[] | Every named step with its job/stage location and declared outputs |
job_edges | object[] | Derived job-level dependsOn edges |
stage_edges | object[] | Derived stage-level dependsOn edges |
outputs_needing_is_output | object[] | Producer steps whose outputs are read cross-step (need isOutput=true) |
Each job_edges / stage_edges entry:
{ "consumer": "Agent", "producer": "Setup" }// means: Agent dependsOn SetupEach step_locations entry:
{ "step": "synthPr", "stage": null, "job": "Setup", "outputs": ["AW_SYNTHETIC_PR_ID"] }Each outputs_needing_is_output entry:
{ "step": "synthPr", "outputs": ["AW_SYNTHETIC_PR_ID"] }Example: inspect output
Section titled “Example: inspect output”Running ado-aw inspect examples/sample-agent.md --json for a simple PR-triggered pipeline returns a summary like:
{ "schema_version": 1, "name": "sample-agent", "shape": "standalone", "body": { "kind": "jobs", "jobs": [ { "id": "Setup", "stage": null, "depends_on": [], ... }, { "id": "Agent", "stage": null, "depends_on": ["Setup"], ... }, { "id": "Detection", "stage": null, "depends_on": ["Agent"], ... }, { "id": "SafeOutputs", "stage": null, "depends_on": ["Detection"], ... }, { "id": "Teardown", "stage": null, "depends_on": ["SafeOutputs"], ... } ] }, "graph": { "job_edges": [ { "consumer": "Agent", "producer": "Setup" }, { "consumer": "Detection", "producer": "Agent" }, { "consumer": "SafeOutputs", "producer": "Detection" }, { "consumer": "Teardown", "producer": "SafeOutputs" } ], ... }}The five jobs reflect the canonical Setup → Agent → Detection → SafeOutputs → Teardown shape for every compiled pipeline.