NoETL Playbook Structure Guide (Canonical v10)
This guide defines the canonical NoETL playbook document structure and how it maps to the runtime model.
Canonical runtime principles (v10):
- Step = admission (
step.spec.policy.admit) + tool pipeline (step.tool) + router (step.nextwith Petri-net arcs) - Retry/pagination/polling/branching inside a step are expressed via task policy:
task.spec.policy.rules - Result handling is reference-first (no special
sinktool kind; “sink” is a pattern) - Runtime scopes:
workload(immutable),ctx(execution-scoped),iter(iteration-scoped),args(token payload), plus pipeline locals (_prev,_task,_attempt,outcome)
Non-canonical removals: There is no playbook-root
vars, nostep.when, no tool-leveleval/expr, and nostep.spec.next_mode. Usespec.policyandnext.specinstead.
Task format: Tasks use the canonical format with explicit
name:field (e.g.,{ name: "task_name", kind: "http", ... }). Tasks without names get synthetic labels (task_0,task_1, ...). See Step Specification for details.
1) Overview
A NoETL playbook is a YAML document that defines a workflow as a set of steps and transitions.
Root sections are limited to:
apiVersionkindmetadatakeychain(optional but recommended)executor(optional)workloadworkflowworkbook(optional)
Root restrictions (canonical):
varsMUST NOT appear at playbook root level.- If credentials are referenced by name (for example
auth: pg_k8s), they SHOULD be declared under rootkeychain. - Any runtime knobs MUST be expressed under
specat their respective scope.
2) Basic structure (canonical v10)
apiVersion: noetl.io/v2
kind: Playbook
metadata:
name: example_playbook
path: workflows/example_playbook
version: "2.0"
description: Example playbook using canonical v10 structure
keychain:
- name: pg_k8s
kind: postgres_credential
executor:
kind: distributed
spec:
pool: default
workload:
api_url: "https://api.example.com"
page_size: 50
workflow:
- step: start
desc: Entry transition (pure routing)
next:
spec: { mode: exclusive }
arcs:
- step: fetch_transform_store
- step: fetch_transform_store
desc: Fetch → transform → store
tool:
- name: fetch_page
kind: http
method: GET
url: "{{ workload.api_url }}/data"
params:
page: 1
pageSize: "{{ workload.page_size }}"
spec:
timeout: { connect: 5, read: 15 }
policy:
rules:
- when: "{{ outcome.status == 'error' and outcome.http.status in [429,500,502,503,504] }}"
then: { do: retry, attempts: 5, backoff: exponential, delay: 2 }
- when: "{{ outcome.status == 'error' }}"
then: { do: fail }
- else:
then: { do: continue }
- name: transform
kind: python
args: { data: "{{ _prev }}" }
code: |
result = {"items": data}
- name: store
kind: postgres
auth: pg_k8s
command: "INSERT INTO ..."
spec:
policy:
rules:
- when: "{{ outcome.status == 'error' and outcome.pg.code in ['40001','40P01'] }}"
then: { do: retry, attempts: 5, backoff: exponential, delay: 2 }
- when: "{{ outcome.status == 'error' }}"
then: { do: fail }
- else:
then: { do: continue }
next:
spec: { mode: exclusive }
arcs:
- step: end
when: "{{ event.name == 'step.done' }}"
- step: end
desc: Terminal transition
tool:
- name: done
kind: noop
3) Metadata
The metadata section describes the playbook itself.
Typical fields:
name(required)path(required)version(recommended)description(recommended)tags,labels(optional)
Example:
metadata:
name: amadeus_ai_api
path: api_integration/amadeus_ai_api
version: "2.0"
description: Complete Amadeus AI travel API integration
tags: [api, travel, llm]
4) Executor (optional)
executor configures where and how the playbook is executed (local vs distributed, worker pool selection, queue backend, etc.).
The section MUST follow the pattern:
executor:
kind: distributed | local | hybrid
spec: { ... }
Executor knobs may be inherited by lower scopes via spec merge precedence (see §6.4).
5) Workload (immutable input)
workload provides default inputs and configuration for the playbook.
At runtime, the server merges:
- playbook
workloaddefaults - execution request payload overrides
This merged workload is immutable and available as workload.* in templates.
6) Workflow (steps and routing)
workflow is a list of steps. Steps form a directed graph using next.arcs[].
6.1 Step = transition (Petri-net)
In canonical v10 there are no special step “types” like start, end, condition.
Instead, behavior is expressed with:
- Admission:
step.spec.policy.admit.rules(server-side) - Execution:
step.toolordered pipeline (worker-side) - Routing:
step.next.spec.mode+step.next.arcs[].when(server-side)
6.2 Step admission (step.spec.policy.admit)
Admission is evaluated by the server before scheduling a step run.
- step: do_work
spec:
policy:
admit:
rules:
- when: "{{ args.enabled == true }}"
then: { allow: true }
- else:
then: { allow: false }
tool: ...
If admission policy is omitted, the step defaults to allow.
6.3 Tool pipeline (step.tool)
A step may contain a tool list which is an ordered pipeline of labeled tasks.
- The worker executes tasks top-to-bottom.
_prevthreads previous task output to the next task (canonical:_prev = outcome.result).- Each task may define
task.spec.policy.rulesto control execution flow inside the pipeline:retry,jump,continue,break,fail
6.4 Spec precedence (canonical)
spec MAY be defined at multiple levels. Inner scopes override outer scopes on overlap.
Recommended precedence for effective task configuration:
kind defaults → executor.spec → step.spec → loop.spec → task.spec
6.5 Routing (step.next router with arcs)
next is a router object that owns routing mode and the arc list.
next:
spec:
mode: exclusive | inclusive # default exclusive
arcs:
- step: success
when: "{{ event.name == 'step.done' }}"
args: { ... } # token payload
- step: failure
when: "{{ event.name == 'step.failed' }}"
If multiple arcs match:
exclusive: first matching arc fires (stable YAML order)inclusive: all matching arcs fire (fan-out)
6.6 Loop (fan-out and iteration)
A step may define loop to repeat its pipeline over a collection.
- step: per_endpoint
loop:
spec:
mode: parallel
max_in_flight: 10
in: "{{ workload.endpoints }}"
iterator: endpoint
tool:
- name: fetch
kind: http
url: "{{ workload.api_url }}{{ iter.endpoint.path }}"
In parallel loop mode:
- each iteration has isolated
iter.* - cross-step writes via
set_ctxmust be restricted until reducers/atomics exist
7) Runtime scopes vs document fields
These are runtime scopes available during evaluation (NOT playbook root keys):
workload.*— immutable merged inputctx.*— execution-scoped mutable context (event-sourced patches)iter.*— loop iteration scoped state (isolated per iteration)args.*— token payload fromnext.arcs[].args- Pipeline locals:
_prev— previous task output_task— current task label_attempt— current attempt numberoutcome— tool outcome envelope (within task policy evaluation)event— boundary event envelope (within routing evaluation)
8) Workbook (optional)
workbook is optional and reserved for a catalog of named reusable tasks/templates.
It is not required for the canonical baseline and may be introduced gradually.
Example placeholder:
workbook:
tasks:
fetch_assessments:
kind: http
method: GET
url: "{{ workload.api_url }}/api/v1/assessments"
9) Common patterns (canonical)
9.1 Retry (task policy)
- name: fetch
kind: http
url: "{{ workload.api_url }}/data"
spec:
policy:
rules:
- when: "{{ outcome.status == 'error' and outcome.http.status in [429,500,502,503,504] }}"
then: { do: retry, attempts: 10, backoff: exponential, delay: 2 }
- when: "{{ outcome.status == 'error' }}"
then: { do: fail }
- else:
then: { do: continue }
9.2 Pagination (jump + iter state)
- name: paginate
kind: noop
spec:
policy:
rules:
- when: "{{ iter.has_more == true }}"
then:
do: jump
to: fetch_page
set_iter:
page: "{{ (iter.page | int) + 1 }}"
- else:
then: { do: break }
9.3 “Sink” (pattern, not a tool kind)
A sink is a storage-writing task in the pipeline that returns a reference (ResultRef).
Links
- DSL Specification (canonical): spec
- Formal Specification (canonical): formal_specification
- Execution Model (canonical): execution_model
- Retry Handling (canonical): retry_mechanism_v2
- Result Storage (canonical): result_storage_canonical_v10
- Pagination (canonical): pagination