A pipeline is a YAML file

A pipeline.yaml file is a source that brings records in and a list of steps the runner executes in order, each naming an adapter

What you'll learn

You've run an example pipeline and want to write or read your own, and predict what each key does before you run it.

  • what each top-level key and each step key does
  • which keys are valid on which kind of step, and how plan checks them
  • why a vendor's field names appear only at the two ends of a file

Here's hello.yaml, an example that ships with gtme, minus its header comment:

It reads three people from a CSV, scores each one, keeps anyone scoring 70 or more, and writes the keepers to out.csv. If you ran See it run, the file is already in your folder, and if it isn't, that page has the two download lines. Ask plan what it read. It spends nothing and sends nothing:

gtme plan hello.yaml

The output is the following:

pipeline hello (version 1)

1. source [source] — csv/source@1
     entity:    person
     writes:    works_at → company (from company_domain)
     provides:  company_domain, email, full_name, title
     est/record: ?

2. score [enrich] — demo/enrich@1
     entity:    person
     reads:     email, full_name
     requires:  any of email | full_name
     provides:  demo.note, demo.score
     cache:     30d
     est/record: $0.0100

3. keep [filter] — sql/filter
     entity:    person
     reads:     (none)
     provides:  (none)
     est/record: ?

4. out [deliver] — csv/deliver@1
     record:    touched → hello
     entity:    person
     reads:     demo.note, demo.score
     requires:  demo.note, demo.score
     provides:  (none)
     idempotency: email
     variables: note ← demo.note, score ← demo.score
     on_missing: skip
     note:      needs namespaced field "demo.note" — this pipeline is coupled to the adapter that provides it
     note:      needs namespaced field "demo.score" — this pipeline is coupled to the adapter that provides it
     est/record: $0.0000

send surface: 1 deliver step(s)
  out → csv/deliver (touch scope: hello)

available fields after the last step: company_domain, demo.note, demo.score, email, full_name, title
plan ok — nothing has been spent

Each entry is a step, with its role in brackets and its adapter's version after @. reads: is what the step reads, its projection: the current value of each field it asked for. requires: must already exist, and provides: is what the step writes. est/record: is the adapter's price per record, and ? means it publishes none. sql/filter and csv/source cost nothing, and an AI step's spend shows on the receipt.

writes: is a relation the source records between each person and their company. touched → hello scopes a delivery to this pipeline, so another pipeline can still deliver the same person. send surface lists every step that sends. The two note: lines aren't a problem to fix. demo.score is namespaced, its name prefixed with the vendor's, and plan is telling you this file depends on demo/enrich.

What you just saw

Two top-level keys name the file and two do the work. name is required, and it's recorded on every run and prefixes the fields the pipeline declares for itself. version is the format version, 1. source: brings records in, and steps: does everything after that.

The source and every step name an adapter with use: and configure it with with:. Plan checks everything under with: against the keys the adapter accepts, so a misspelled key fails before the run starts. On a source, columns: maps your CSV headers to canonical field names, which is how Company Website arrives as company_domain. limit: caps how many records a source emits.

Steps run strictly in order, top to bottom. Each step's id labels its row in the plan and the receipt. Steps don't hand records to each other. Each reads from the ledger and writes back, so keep reads the demo.score that score wrote.

A deliver step like out is an ordinary entry, so a pipeline can have none, one, or several, anywhere in the list (the decision record, ADR-031).

A step's other keys say what it reads, writes, and remembers, and when it runs. Which keys are valid depends on the adapter's role, and plan rejects the rest.

KeyValid onWhat it does
uses:Filter, compose, and review steps, as Steps and roles defines themLists the fields the step reads. Plan checks that an earlier step provides each one, and the step sees only these.
provides:The same stepsDeclares the fields the step writes. A bare name like subject lands as hello.subject, prefixed with the pipeline's name.
template: under with:The same stepsHolds the step's text: a prompt for an ai/* step, or copy rendered per record for text/compose. It's a string or {file: path}.
when:Any step after the sourceTakes STEP_ID.passed, where STEP_ID is an earlier filter step. A fail verdict already stops a record at the filter, so when: holds the records it never judged, like one skipped for a missing field. It also shows a reviewer that the paid step comes after the judgment.
cache:Enrich, verify, and AI stepsSets a freshness window like 30d, overriding the adapter's default. Enrich and verify steps skip a record whose value is still current, and an AI step reuses a judgment only that long.
variables:Deliver stepsMaps the target's field names to ledger fields, as in score: demo.score.
idempotency:Deliver stepsNames the field that keys a delivery, defaulting to the identity key. A later run skips the same value to the same target (the exception is a target that updates in place, ADR-045).

So what?

Plan checks every field name before anything spends. Here's a text/compose step added to hello.yaml between keep and out. It writes an email subject from a template, at no cost:

  - id: subject
    use: text/compose
    uses: [full_name, company_name]
    provides: [subject]
    with:
      template: "{{ record.full_name }}, a note for {{ record.company_name }}"
gtme plan hello.yaml

The output is the following:

gtme: step "subject": needs company_name, which no earlier step provides (available: company_domain, demo.note, demo.score, email, full_name, title)

The CSV has a company domain and no company name, so nothing upstream provides it. Change both mentions of company_name to company_domain, run plan again, and the new step reads:

4. subject [compose] — text/compose@1
     entity:    person
     reads:     full_name, company_domain
     requires:  full_name, company_domain
     provides:  hello.subject
     est/record: ?

provides: [subject] became hello.subject. To write it to the file, add subject: hello.subject under the out step's variables:. That step and that variables: line are what you'd hand your agent.

Running the edited file again doesn't write Jane a second time, because out.csv isn't a target that updates in place. gtme help --agent prints every installed adapter's keys and fields, the same list as the Adapter catalog.

That's it. Whatever file you or your agent writes next gets the same check before it spends anything.

Why it's this way

Fields a prompt or template mentions are declared, so plan can check them. An adapter's manifest, its declaration of what it needs and provides, can't know which fields a prompt or an email template mentions. ADR-004 added uses: for AI steps, and ADR-019 applied the same rule to variables: on deliver steps. The cost is typing a field name twice, in the text and in uses:.

A vendor's field names appear only at the two ends. columns: on the source and variables: on a deliver step are the only places a file maps someone else's names to gtme's (ADR-018). Every step between them uses canonical or namespaced names, so plan can prove the file holds together. A computed field gets its own sql/transform step.

The runner accepts only the keys the schema lists. SPEC.md §9 is the whole format, with no loops and no branches. We think that's the right trade for a file an agent writes and a person approves.

The gap we notice is routing the records a filter rejected. Because when: names only .passed, sending rejects somewhere else takes a second sql/filter over the verdict the ledger already holds. That works, and reads less plainly than a .failed gate would.

Where it shows up