dirigent-examples 0.15.0__py3-none-any.whl

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.
Files changed (216) hide show
  1. dirigent_examples/__init__.py +22 -0
  2. dirigent_examples/py.typed +0 -0
  3. dirigent_examples/shelves/README.md +299 -0
  4. dirigent_examples/shelves/composition/README.md +18 -0
  5. dirigent_examples/shelves/composition/chained-instances.yaml +97 -0
  6. dirigent_examples/shelves/composition/composition-child.yaml +64 -0
  7. dirigent_examples/shelves/composition/composition-parent.yaml +89 -0
  8. dirigent_examples/shelves/connections.yaml +52 -0
  9. dirigent_examples/shelves/demo/README.md +19 -0
  10. dirigent_examples/shelves/demo/markdown-showcase.yaml +117 -0
  11. dirigent_examples/shelves/demo/params-showcase.yaml +92 -0
  12. dirigent_examples/shelves/demo/requires.yaml +65 -0
  13. dirigent_examples/shelves/demo/weekly-import-malawi.yaml +54 -0
  14. dirigent_examples/shelves/demo/weekly-import-nepal.yaml +75 -0
  15. dirigent_examples/shelves/docker/README.md +29 -0
  16. dirigent_examples/shelves/docker/docker-build-push.yaml +110 -0
  17. dirigent_examples/shelves/docker/docker-build-run.yaml +106 -0
  18. dirigent_examples/shelves/docker/docker-compose-database.yaml +124 -0
  19. dirigent_examples/shelves/docker/docker-compose-failing-up.yaml +83 -0
  20. dirigent_examples/shelves/docker/docker-compose-file.yaml +117 -0
  21. dirigent_examples/shelves/docker/docker-compose-profiles-env.yaml +133 -0
  22. dirigent_examples/shelves/docker/docker-compose-stack.yaml +70 -0
  23. dirigent_examples/shelves/docker/docker-hello.yaml +53 -0
  24. dirigent_examples/shelves/docker/docker-remote-daemon.yaml +92 -0
  25. dirigent_examples/shelves/docker/docker-run-failing-teardown.yaml +94 -0
  26. dirigent_examples/shelves/docker/docker-ticker.yaml +49 -0
  27. dirigent_examples/shelves/execute/README.md +16 -0
  28. dirigent_examples/shelves/execute/long-log.yaml +89 -0
  29. dirigent_examples/shelves/failure/README.md +20 -0
  30. dirigent_examples/shelves/failure/error-handler.yaml +89 -0
  31. dirigent_examples/shelves/failure/optional-step.yaml +82 -0
  32. dirigent_examples/shelves/failure/retries.yaml +75 -0
  33. dirigent_examples/shelves/failure/retry-budget.yaml +82 -0
  34. dirigent_examples/shelves/failure/step-timeout.yaml +96 -0
  35. dirigent_examples/shelves/git/README.md +32 -0
  36. dirigent_examples/shelves/git/git-checkout-build.yaml +125 -0
  37. dirigent_examples/shelves/git/git-checkout-compose.yaml +138 -0
  38. dirigent_examples/shelves/git/git-checkout-public.yaml +84 -0
  39. dirigent_examples/shelves/graph/README.md +22 -0
  40. dirigent_examples/shelves/graph/deep-chain.yaml +119 -0
  41. dirigent_examples/shelves/graph/fan-in.yaml +80 -0
  42. dirigent_examples/shelves/graph/fan-out.yaml +66 -0
  43. dirigent_examples/shelves/graph/linear.yaml +66 -0
  44. dirigent_examples/shelves/graph/parallel-branches.yaml +57 -0
  45. dirigent_examples/shelves/graph/parallel-sleep.yaml +55 -0
  46. dirigent_examples/shelves/graph/skip-diamond.yaml +105 -0
  47. dirigent_examples/shelves/graph/wide-fan.yaml +147 -0
  48. dirigent_examples/shelves/hello-world.yaml +30 -0
  49. dirigent_examples/shelves/open-data/README.md +67 -0
  50. dirigent_examples/shelves/open-data/feeds-composition.yaml +120 -0
  51. dirigent_examples/shelves/open-data/gdacs-disaster-updates.yaml +230 -0
  52. dirigent_examples/shelves/open-data/github-releases-relay.yaml +225 -0
  53. dirigent_examples/shelves/open-data/hdx-dataset-watch.yaml +211 -0
  54. dirigent_examples/shelves/open-data/kobo-submissions-to-csv.yaml +149 -0
  55. dirigent_examples/shelves/open-data/nominatim-geocode-facilities.yaml +169 -0
  56. dirigent_examples/shelves/open-data/odk-central-submissions.yaml +146 -0
  57. dirigent_examples/shelves/open-data/open-meteo-weekly-report.yaml +142 -0
  58. dirigent_examples/shelves/open-data/overpass-health-facilities.yaml +154 -0
  59. dirigent_examples/shelves/open-data/usgs-earthquakes-alert.yaml +208 -0
  60. dirigent_examples/shelves/open-data/who-gho-indicators-to-parquet.yaml +163 -0
  61. dirigent_examples/shelves/open-data/wikidata-country-reference.yaml +146 -0
  62. dirigent_examples/shelves/open-data/world-bank-population-trend.yaml +166 -0
  63. dirigent_examples/shelves/patterns/README.md +144 -0
  64. dirigent_examples/shelves/patterns/concurrency-queue.yaml +89 -0
  65. dirigent_examples/shelves/patterns/concurrency-replace.yaml +92 -0
  66. dirigent_examples/shelves/patterns/concurrency-skip.yaml +97 -0
  67. dirigent_examples/shelves/patterns/connections-referenced-vs-carried.yaml +140 -0
  68. dirigent_examples/shelves/patterns/deadline-on-a-sensor.yaml +106 -0
  69. dirigent_examples/shelves/patterns/fan-out-continue.yaml +88 -0
  70. dirigent_examples/shelves/patterns/fan-out-fail-fast.yaml +80 -0
  71. dirigent_examples/shelves/patterns/fan-out-from-params.yaml +84 -0
  72. dirigent_examples/shelves/patterns/fan-out-item-wise.yaml +111 -0
  73. dirigent_examples/shelves/patterns/fan-out-literal-list.yaml +81 -0
  74. dirigent_examples/shelves/patterns/fan-out-nested-objects.yaml +107 -0
  75. dirigent_examples/shelves/patterns/fan-out-then-join.yaml +86 -0
  76. dirigent_examples/shelves/patterns/log-levels.yaml +119 -0
  77. dirigent_examples/shelves/patterns/outputs-inline-vs-storage.yaml +140 -0
  78. dirigent_examples/shelves/patterns/params-every-type.yaml +259 -0
  79. dirigent_examples/shelves/patterns/params-validation-refuses.yaml +131 -0
  80. dirigent_examples/shelves/patterns/pipeline-run-child.yaml +96 -0
  81. dirigent_examples/shelves/patterns/pipeline-run-fire-and-forget.yaml +99 -0
  82. dirigent_examples/shelves/patterns/pipeline-run-strict.yaml +103 -0
  83. dirigent_examples/shelves/patterns/pipeline-run-wait.yaml +107 -0
  84. dirigent_examples/shelves/patterns/pipeline-run-with-params.yaml +124 -0
  85. dirigent_examples/shelves/patterns/poll-cadence.yaml +102 -0
  86. dirigent_examples/shelves/patterns/priority-layered.yaml +120 -0
  87. dirigent_examples/shelves/patterns/references-cheat-sheet.yaml +186 -0
  88. dirigent_examples/shelves/patterns/retry-budget-exhausted.yaml +92 -0
  89. dirigent_examples/shelves/patterns/retry-exponential-backoff.yaml +88 -0
  90. dirigent_examples/shelves/patterns/retry-only-transient.yaml +108 -0
  91. dirigent_examples/shelves/patterns/retry-with-jitter.yaml +102 -0
  92. dirigent_examples/shelves/patterns/rule-all-done.yaml +83 -0
  93. dirigent_examples/shelves/patterns/rule-all-success.yaml +79 -0
  94. dirigent_examples/shelves/patterns/rule-always.yaml +92 -0
  95. dirigent_examples/shelves/patterns/rule-one-failed.yaml +86 -0
  96. dirigent_examples/shelves/patterns/schedule-at-once.yaml +110 -0
  97. dirigent_examples/shelves/patterns/schedule-cron-timezone.yaml +121 -0
  98. dirigent_examples/shelves/patterns/schedule-interval.yaml +109 -0
  99. dirigent_examples/shelves/patterns/schedule-window-half-open.yaml +105 -0
  100. dirigent_examples/shelves/patterns/sensor-http-ready.yaml +121 -0
  101. dirigent_examples/shelves/patterns/sensor-storage-exists.yaml +134 -0
  102. dirigent_examples/shelves/patterns/step-names-and-keys.yaml +99 -0
  103. dirigent_examples/shelves/patterns/timeout-fails-the-step.yaml +94 -0
  104. dirigent_examples/shelves/patterns/timeout-skips-the-step.yaml +102 -0
  105. dirigent_examples/shelves/patterns/webhook-mapping-nested-payload.yaml +125 -0
  106. dirigent_examples/shelves/patterns/webhook-signed.yaml +144 -0
  107. dirigent_examples/shelves/preview/s3-parquet-to-ingestion.yaml +92 -0
  108. dirigent_examples/shelves/python/README.md +31 -0
  109. dirigent_examples/shelves/python/apply_and_run.py +52 -0
  110. dirigent_examples/shelves/python/ci_gate.py +76 -0
  111. dirigent_examples/shelves/python/connections.py +61 -0
  112. dirigent_examples/shelves/python/error_handling.py +84 -0
  113. dirigent_examples/shelves/python/follow_logs.py +39 -0
  114. dirigent_examples/shelves/python/list_and_filter.py +52 -0
  115. dirigent_examples/shelves/queues/README.md +59 -0
  116. dirigent_examples/shelves/queues/kafka-consume-then-transform.yaml +105 -0
  117. dirigent_examples/shelves/queues/kafka-produce-then-consume.yaml +124 -0
  118. dirigent_examples/shelves/queues/rabbitmq-consume-ack-on-success.yaml +102 -0
  119. dirigent_examples/shelves/queues/report-to-kafka.yaml +105 -0
  120. dirigent_examples/shelves/queues/report-to-rabbitmq.yaml +105 -0
  121. dirigent_examples/shelves/recipes/README.md +130 -0
  122. dirigent_examples/shelves/recipes/csv-header-rules.yaml +147 -0
  123. dirigent_examples/shelves/recipes/csv-to-ndjson.yaml +107 -0
  124. dirigent_examples/shelves/recipes/etl-csv-clean-validate-parquet.yaml +207 -0
  125. dirigent_examples/shelves/recipes/filter-by-predicate.yaml +116 -0
  126. dirigent_examples/shelves/recipes/filter-then-map-then-reduce.yaml +109 -0
  127. dirigent_examples/shelves/recipes/http-fetch-validate-post.yaml +142 -0
  128. dirigent_examples/shelves/recipes/http-follow-redirects.yaml +100 -0
  129. dirigent_examples/shelves/recipes/http-get-with-query.yaml +96 -0
  130. dirigent_examples/shelves/recipes/http-headers-and-auth-connection.yaml +110 -0
  131. dirigent_examples/shelves/recipes/http-post-file-from-storage.yaml +117 -0
  132. dirigent_examples/shelves/recipes/http-post-json-echo.yaml +105 -0
  133. dirigent_examples/shelves/recipes/http-post-report.yaml +183 -0
  134. dirigent_examples/shelves/recipes/http-save-body-to-storage.yaml +106 -0
  135. dirigent_examples/shelves/recipes/http-success-status-list.yaml +80 -0
  136. dirigent_examples/shelves/recipes/http-timeout-override.yaml +104 -0
  137. dirigent_examples/shelves/recipes/jq-dedupe-by-key.yaml +78 -0
  138. dirigent_examples/shelves/recipes/jq-defaults-and-nulls.yaml +91 -0
  139. dirigent_examples/shelves/recipes/jq-group-by-and-sum.yaml +74 -0
  140. dirigent_examples/shelves/recipes/jq-join-two-lists.yaml +77 -0
  141. dirigent_examples/shelves/recipes/jq-long-to-wide.yaml +76 -0
  142. dirigent_examples/shelves/recipes/jq-nested-to-flat.yaml +89 -0
  143. dirigent_examples/shelves/recipes/jq-pivot-wide-to-long.yaml +65 -0
  144. dirigent_examples/shelves/recipes/jq-running-totals.yaml +82 -0
  145. dirigent_examples/shelves/recipes/jq-string-cleaning.yaml +88 -0
  146. dirigent_examples/shelves/recipes/jq-top-n.yaml +85 -0
  147. dirigent_examples/shelves/recipes/jq-validate-in-jq-vs-schema.yaml +124 -0
  148. dirigent_examples/shelves/recipes/jq-window-dates.yaml +82 -0
  149. dirigent_examples/shelves/recipes/json-to-csv-flattening.yaml +141 -0
  150. dirigent_examples/shelves/recipes/large-output-to-storage.yaml +134 -0
  151. dirigent_examples/shelves/recipes/map-enrich-with-lookup.yaml +96 -0
  152. dirigent_examples/shelves/recipes/ndjson-to-parquet.yaml +130 -0
  153. dirigent_examples/shelves/recipes/pagination-by-fan-out.yaml +115 -0
  154. dirigent_examples/shelves/recipes/parquet-round-trip-types.yaml +163 -0
  155. dirigent_examples/shelves/recipes/reconcile-two-sources.yaml +159 -0
  156. dirigent_examples/shelves/recipes/report-built-in.yaml +72 -0
  157. dirigent_examples/shelves/recipes/report-daily-digest.yaml +186 -0
  158. dirigent_examples/shelves/recipes/report-to-file.yaml +131 -0
  159. dirigent_examples/shelves/recipes/report-to-webhook.yaml +136 -0
  160. dirigent_examples/shelves/recipes/schema-carried.yaml +112 -0
  161. dirigent_examples/shelves/recipes/schema-formats.yaml +107 -0
  162. dirigent_examples/shelves/recipes/schema-referenced.yaml +86 -0
  163. dirigent_examples/shelves/recipes/schema-refuses-then-rule.yaml +127 -0
  164. dirigent_examples/shelves/recipes/storage-copy-dated-archive.yaml +114 -0
  165. dirigent_examples/shelves/recipes/storage-exists-gate.yaml +127 -0
  166. dirigent_examples/shelves/recipes/storage-manifest-of-a-fan-out.yaml +104 -0
  167. dirigent_examples/shelves/recipes/storage-write-then-read.yaml +119 -0
  168. dirigent_examples/shelves/recipes/webhook-post-hmac.yaml +132 -0
  169. dirigent_examples/shelves/recipes/webhook-post-summary.yaml +142 -0
  170. dirigent_examples/shelves/s3/README.md +34 -0
  171. dirigent_examples/shelves/s3/report-to-s3.yaml +93 -0
  172. dirigent_examples/shelves/s3/s3-copy-and-verify.yaml +105 -0
  173. dirigent_examples/shelves/s3/s3-csv-report.yaml +87 -0
  174. dirigent_examples/shelves/s3/s3-parquet-report.yaml +77 -0
  175. dirigent_examples/shelves/s3/s3-round-trip.yaml +125 -0
  176. dirigent_examples/shelves/schemas/README.md +36 -0
  177. dirigent_examples/shelves/schemas/echo-reading.json +18 -0
  178. dirigent_examples/shelves/schemas/ou-record.json +13 -0
  179. dirigent_examples/shelves/schemas/station-reading.json +13 -0
  180. dirigent_examples/shelves/sensors/README.md +16 -0
  181. dirigent_examples/shelves/sensors/sensor-gate.yaml +65 -0
  182. dirigent_examples/shelves/sensors/time-window.yaml +61 -0
  183. dirigent_examples/shelves/sql/README.md +52 -0
  184. dirigent_examples/shelves/sql/duckdb-parquet-to-report.yaml +146 -0
  185. dirigent_examples/shelves/sql/sql-postgres-readonly.yaml +111 -0
  186. dirigent_examples/shelves/sql/sql-query-to-storage.yaml +85 -0
  187. dirigent_examples/shelves/sql/sql-sqlite-roundtrip.yaml +114 -0
  188. dirigent_examples/shelves/sql/warehouse.sql +42 -0
  189. dirigent_examples/shelves/transform/README.md +36 -0
  190. dirigent_examples/shelves/transform/csv-report.yaml +55 -0
  191. dirigent_examples/shelves/transform/jq-filter-and-map.yaml +70 -0
  192. dirigent_examples/shelves/transform/jq-group-and-aggregate.yaml +70 -0
  193. dirigent_examples/shelves/transform/jq-join-two-sources.yaml +98 -0
  194. dirigent_examples/shelves/transform/jq-reshape.yaml +91 -0
  195. dirigent_examples/shelves/transform/jq-stream-through-storage.yaml +112 -0
  196. dirigent_examples/shelves/transform/ndjson-round-trip.yaml +56 -0
  197. dirigent_examples/shelves/transform/parquet-round-trip.yaml +68 -0
  198. dirigent_examples/shelves/transform/std-convert-fan-out.yaml +142 -0
  199. dirigent_examples/shelves/transform/xml-feed-to-ndjson.yaml +116 -0
  200. dirigent_examples/shelves/transform/yaml-config-to-json.yaml +104 -0
  201. dirigent_examples/shelves/triggers/README.md +45 -0
  202. dirigent_examples/shelves/triggers/at-one-time.yaml +78 -0
  203. dirigent_examples/shelves/triggers/cron-nightly.yaml +79 -0
  204. dirigent_examples/shelves/triggers/cron-windowed.yaml +86 -0
  205. dirigent_examples/shelves/triggers/document-nightly.yaml +80 -0
  206. dirigent_examples/shelves/triggers/interval-rolling.yaml +88 -0
  207. dirigent_examples/shelves/triggers/managed-and-manual.yaml +109 -0
  208. dirigent_examples/shelves/triggers/webhook-trigger.yaml +75 -0
  209. dirigent_examples/shelves/validate/README.md +31 -0
  210. dirigent_examples/shelves/validate/expects-a-shape.yaml +56 -0
  211. dirigent_examples/shelves/validate/the-shape-is-wrong.yaml +46 -0
  212. dirigent_examples-0.15.0.dist-info/METADATA +21 -0
  213. dirigent_examples-0.15.0.dist-info/RECORD +216 -0
  214. dirigent_examples-0.15.0.dist-info/WHEEL +4 -0
  215. dirigent_examples-0.15.0.dist-info/entry_points.txt +3 -0
  216. dirigent_examples-0.15.0.dist-info/licenses/LICENSE +18 -0
@@ -0,0 +1,146 @@
1
+ # The country code table every integration ends up needing, built from Wikidata rather than typed.
2
+ #
3
+ # Wikidata's SPARQL endpoint is public and keyless. It answers SPARQL over the whole of
4
+ # Wikidata, and with `Accept: application/sparql-results+json` the answer is the W3C results
5
+ # shape: {"head": {"vars": [...]}, "results": {"bindings": [...]}}, where every binding is a
6
+ # map from variable name to {"type": ..., "value": ...} -- and every value is a *string*, even
7
+ # the numeric ones. Unwrapping that envelope is most of the work below.
8
+ #
9
+ # The query asks for sovereign states with an ISO 3166-1 alpha-3 code, plus the alpha-2 and
10
+ # numeric codes and the English label. The result is the reference table other pipelines join
11
+ # against: WHO GHO speaks alpha-3, health information systems often carry alpha-2, and national statistics
12
+ # offices publish the numeric code.
13
+ #
14
+ # On manners: the endpoint asks every client for a descriptive User-Agent that says who is
15
+ # calling and how to reach them, and it will refuse an anonymous one under load. That header is
16
+ # not optional politeness, it is the terms of use. A query heavier than this one belongs behind
17
+ # a longer timeout, or in the Query Service's own bulk export.
18
+ #
19
+ # What happens, hop by hop:
20
+ #
21
+ # fetch one GET with the query in the `query` parameter and the answer negotiated to
22
+ # JSON by the Accept header.
23
+ # table the unwrapping: `.results.bindings` mapped to flat rows, each `.value` pulled
24
+ # out of its little envelope, the numeric code turned into an actual number, and
25
+ # the Wikidata entity URI cut down to its Q-id.
26
+ # publish storage.write, the table as json. A reference table is a thing other runs read
27
+ # rather than a value this run carries onward, and a value leaves the run here.
28
+ # recalled storage.read of that object: the read another pipeline would do against the same
29
+ # path, which is the only way a value comes back in.
30
+ # summary what the run reports, counted off what came back rather than off what was held
31
+ # in memory upstream.
32
+ #
33
+ # To make it yours: change the query. Swapping wd:Q3624078 for wd:Q6256 widens it from
34
+ # sovereign states to countries, and adding `?item wdt:P1082 ?population` gives every row a
35
+ # population without another request. The table is written into the run's own scratch, which is
36
+ # deleted with the run; on an instance, writing to s3:// or to a stable prefix is what makes the
37
+ # table outlive the run that built it. A reference is not a parameter with a default, because a
38
+ # parameter default is literal text: ${run.scratch} means nothing until a step resolves it.
39
+ #
40
+ # dg run --local examples/open-data/wikidata-country-reference.yaml
41
+
42
+ format: dirigent/v1
43
+ kind: pipeline
44
+ code: wikidata-country-reference
45
+ name: Country code reference from Wikidata
46
+ description: |
47
+ ISO 3166-1 alpha-2, alpha-3 and numeric codes for every sovereign state, read from
48
+ **Wikidata** over SPARQL and saved as a json reference table other pipelines read.
49
+
50
+ tags: [open-data, http, storage, transform, sparql]
51
+
52
+ requires:
53
+ blocks:
54
+ - http.request
55
+ - transform.jq
56
+ - storage.write
57
+ - storage.read
58
+
59
+ params:
60
+ type: object
61
+ additionalProperties: false
62
+ properties:
63
+ language:
64
+ type: string
65
+ default: en
66
+ description: The label language, as the Wikidata label service names it.
67
+ user_agent:
68
+ type: string
69
+ default: dirigent-example/1.0 (https://github.com/winterop-com/dirigent)
70
+ description: Required by the endpoint's terms of use; say who you are and how to be reached.
71
+
72
+ steps:
73
+ fetch:
74
+ block: http.request
75
+ # The endpoint queues queries behind a shared budget and answers 429 when it is busy, so
76
+ # the wait between tries is longer than the query itself takes.
77
+ retry:
78
+ max_attempts: 3
79
+ backoff: 20s
80
+ config:
81
+ url: https://query.wikidata.org/sparql
82
+ headers:
83
+ # Without this the answer is XML, which is the endpoint's default.
84
+ Accept: application/sparql-results+json
85
+ User-Agent: ${params.user_agent}
86
+ query:
87
+ query: |
88
+ SELECT ?item ?itemLabel ?iso2 ?iso3 ?isoNumeric WHERE {
89
+ ?item wdt:P31 wd:Q3624078 .
90
+ ?item wdt:P298 ?iso3 .
91
+ OPTIONAL { ?item wdt:P297 ?iso2 }
92
+ OPTIONAL { ?item wdt:P299 ?isoNumeric }
93
+ SERVICE wikibase:label { bd:serviceParam wikibase:language "${params.language}". }
94
+ }
95
+ ORDER BY ?iso3
96
+ # A cold query against a busy endpoint can take most of a minute.
97
+ timeout: 90s
98
+
99
+ table:
100
+ block: transform.jq
101
+ depends_on: [fetch]
102
+ config:
103
+ input: ${steps.fetch.output.body}
104
+ # Every binding is {type, value} and every value is a string, so the numeric code is
105
+ # converted here rather than being carried as "454" for the rest of its life. An
106
+ # OPTIONAL that matched nothing has no binding at all, which is why each read is
107
+ # guarded rather than defaulted after the fact.
108
+ program: |
109
+ [.results.bindings[]
110
+ | {
111
+ wikidata_id: (.item.value | sub("^.*/"; "")),
112
+ name: .itemLabel.value,
113
+ iso2: (if .iso2 then .iso2.value else null end),
114
+ iso3: .iso3.value,
115
+ iso_numeric: (if .isoNumeric then (.isoNumeric.value | tonumber) else null end)
116
+ }]
117
+ | unique_by(.iso3)
118
+
119
+ publish:
120
+ block: storage.write
121
+ depends_on: [table]
122
+ config:
123
+ target: ${run.scratch}/countries.json
124
+ value: ${steps.table.output.value}
125
+
126
+ recalled:
127
+ block: storage.read
128
+ depends_on: [publish]
129
+ config:
130
+ # The read another pipeline would do: the table by its path, and the count below is off
131
+ # the object rather than off the value upstream.
132
+ source: ${steps.publish.output.uri}
133
+ max_size: 4mb
134
+
135
+ summary:
136
+ block: transform.jq
137
+ depends_on: [recalled]
138
+ config:
139
+ input: ${steps.recalled.output.value}
140
+ program: |
141
+ {
142
+ countries: length,
143
+ with_iso2: ([.[] | select(.iso2 != null)] | length),
144
+ with_numeric: ([.[] | select(.iso_numeric != null)] | length),
145
+ sample: (sort_by(.iso3) | .[0:3])
146
+ }
@@ -0,0 +1,166 @@
1
+ # Sixty years of population for one country, with year-on-year growth worked out on the way past.
2
+ #
3
+ # The World Bank's indicator API is keyless and answers a two-element array: the first element
4
+ # is the paging envelope, {page, pages, per_page, total, lastupdated}, and the second is the list
5
+ # of observations, newest year first. Nothing is nested inside anything else, so `.[0]` and
6
+ # `.[1]` are the whole access pattern -- and reading a page count out of `.[0]` is what makes
7
+ # paging visible rather than assumed.
8
+ #
9
+ # On paging: dirigent's format has no loop, so "keep fetching until the pages run out" is not a
10
+ # step. The honest single-hop version is the one below: ask for a page large enough to hold the
11
+ # whole series, then check the envelope and refuse the run if the answer says there was more.
12
+ # A truncated series that looks complete is the failure worth spending a step on.
13
+ #
14
+ # What happens, hop by hop:
15
+ #
16
+ # fetch one GET. per_page is a parameter because it is the thing you raise when the
17
+ # series is longer than the default guess.
18
+ # whole the paging check. jq's error() fails the step, so a series that did not fit
19
+ # stops here instead of reaching the gate looking well-formed.
20
+ # trend the arithmetic. The API answers newest-first, so the series is reversed into
21
+ # chronological order and each row is paired with the one before it. A missing
22
+ # year, or the first year, has no growth to report, and gets null rather than
23
+ # zero: zero would mean the population did not change.
24
+ # gate validate.schema against the shape written below. The gate passes the value
25
+ # through, so the step below reads the gate rather than the trend, and the document
26
+ # itself records that nothing reached the csv without being checked.
27
+ # checked storage.write, the checked rows as json. A converter reads one URI and writes
28
+ # another, so the only object the csv can be made from is the one the gate passed.
29
+ # report json to csv, from that object.
30
+ #
31
+ # To make it yours: change country to any ISO3 code and indicator to any World Bank series code
32
+ # -- SP.DYN.LE00.IN for life expectancy, NY.GDP.PCAP.CD for GDP per head -- and widen the
33
+ # schema's population bounds if the indicator you pick is not a count of people.
34
+ #
35
+ # dg run --local examples/open-data/world-bank-population-trend.yaml
36
+ # dg run --local examples/open-data/world-bank-population-trend.yaml -p country=NPL -p per_page=40
37
+
38
+ format: dirigent/v1
39
+ kind: pipeline
40
+ code: world-bank-population-trend
41
+ name: World Bank population trend
42
+ description: |
43
+ One country's population series from the **World Bank indicator API**, with year-on-year
44
+ growth computed in jq, checked against a carried schema and written as csv.
45
+
46
+ A server refuses a document that carries a schema, so this one is for a `--local` or
47
+ standalone run; on an instance, the same shape is created once with `dg schema create`.
48
+
49
+ tags: [open-data, http, storage, transform, validate, csv]
50
+
51
+ requires:
52
+ blocks:
53
+ - http.request
54
+ - transform.jq
55
+ - validate.schema
56
+ - storage.write
57
+ - convert.std
58
+
59
+ # Carried in the document so the gate needs nothing handed to it. The bounds are not
60
+ # decoration: a year outside them, or a negative population, means the API moved under us.
61
+ schemas:
62
+ population-trend-row:
63
+ type: array
64
+ minItems: 1
65
+ items:
66
+ type: object
67
+ required: [country, iso3, year, population, growth_pct]
68
+ additionalProperties: false
69
+ properties:
70
+ country: { type: string }
71
+ iso3: { type: string, minLength: 3, maxLength: 3 }
72
+ year: { type: integer, minimum: 1960, maximum: 2100 }
73
+ population: { type: [integer, "null"], minimum: 0 }
74
+ growth_pct: { type: [number, "null"] }
75
+
76
+ params:
77
+ type: object
78
+ additionalProperties: false
79
+ properties:
80
+ country:
81
+ type: string
82
+ default: MWI
83
+ description: ISO3 country code, as it appears in the request path.
84
+ indicator:
85
+ type: string
86
+ default: SP.POP.TOTL
87
+ description: A World Bank series code; the default is total population.
88
+ per_page:
89
+ type: integer
90
+ default: 100
91
+ minimum: 1
92
+ description: Raise this when the paging check refuses a series longer than one page.
93
+
94
+ steps:
95
+ fetch:
96
+ block: http.request
97
+ config:
98
+ url: https://api.worldbank.org/v2/country/${params.country}/indicator/${params.indicator}
99
+ query:
100
+ # Without format=json the API answers XML, which is its own default and nobody's
101
+ # convenience.
102
+ format: json
103
+ per_page: ${params.per_page}
104
+ page: 1
105
+
106
+ whole:
107
+ block: transform.jq
108
+ depends_on: [fetch]
109
+ config:
110
+ input: ${steps.fetch.output.body}
111
+ # The envelope, checked and then dropped: everything below works on the observations.
112
+ program: |
113
+ .[0] as $meta
114
+ | if $meta.pages > 1
115
+ then error("the series has \($meta.pages) pages of \($meta.per_page); raise per_page to at least \($meta.total)")
116
+ else .[1]
117
+ end
118
+
119
+ trend:
120
+ block: transform.jq
121
+ depends_on: [whole]
122
+ config:
123
+ input: ${steps.whole.output.value}
124
+ # Chronological order first, because "the year before" is meaningless in the order the
125
+ # API answers in. The window is a pair, so index 0 has no predecessor and no growth.
126
+ program: |
127
+ [.[] | {country: .country.value,
128
+ iso3: .countryiso3code,
129
+ year: (.date | tonumber),
130
+ population: .value}]
131
+ | sort_by(.year)
132
+ | . as $series
133
+ | [range($series | length) as $i
134
+ | $series[$i]
135
+ | . + {growth_pct:
136
+ (if $i == 0 then null
137
+ else ($series[$i - 1].population) as $before
138
+ | if $before == null or .population == null or $before == 0 then null
139
+ else ((.population - $before) / $before * 1000 | round / 10)
140
+ end
141
+ end)}]
142
+
143
+ gate:
144
+ block: validate.schema
145
+ depends_on: [trend]
146
+ config:
147
+ input: ${steps.trend.output.value}
148
+ schema: population-trend-row
149
+
150
+ checked:
151
+ block: storage.write
152
+ depends_on: [gate]
153
+ config:
154
+ # Reading the gate rather than the trend is what proves, in the document, that only a
155
+ # checked value reaches the csv.
156
+ target: ${run.scratch}/${params.country}-${params.indicator}.json
157
+ value: ${steps.gate.output.value}
158
+
159
+ report:
160
+ block: convert.std
161
+ depends_on: [checked]
162
+ config:
163
+ source: ${steps.checked.output.uri}
164
+ target: ${run.scratch}/${params.country}-${params.indicator}.csv
165
+ from: json
166
+ to: csv
@@ -0,0 +1,144 @@
1
+ # Engine patterns
2
+
3
+ One engine behaviour per file, named for what it shows. Where the other shelves are organised
4
+ by what a pipeline is *for* -- moving data, reshaping it, waiting for it -- this one is
5
+ organised by what the engine *does*: the edge conditions, the retry policy, the two clocks, the
6
+ item policies, the trigger declarations, the concurrency policies, and the reference language.
7
+
8
+ Every document here runs for real. The HTTP ones call [Postman Echo](https://postman-echo.com),
9
+ a public request-and-response service, so nothing has to be stood up first; the rest are
10
+ offline. None of them needs `--enable-unsafe`: no file on this shelf runs code on the worker.
11
+
12
+ Each header says what the pipeline demonstrates end to end, what each hop hands on, what to
13
+ change, and **what outcome to expect** -- because several of these fail on purpose, and a file
14
+ whose lesson is a failure has to say so before you run it.
15
+
16
+ ## Trigger rules
17
+
18
+ The edge condition on `depends_on`. Each of these fails a step deliberately, with
19
+ `/status/500`, so the rule has something to react to.
20
+
21
+ | File | What it shows | Ends as |
22
+ | --- | --- | --- |
23
+ | [rule-all-success.yaml](rule-all-success.yaml) | The default: a failure skips everything below it | `failed` |
24
+ | [rule-all-done.yaml](rule-all-done.yaml) | Cleanup that runs whichever way the branch went | `failed` |
25
+ | [rule-one-failed.yaml](rule-one-failed.yaml) | The handler branch, and the success branch it excludes | `failed` |
26
+ | [rule-always.yaml](rule-always.yaml) | The widest edge, and the ordering it still respects | `failed` |
27
+
28
+ ## Retries
29
+
30
+ Whether a failed attempt earns another one, and how long it waits first.
31
+
32
+ | File | What it shows | Ends as |
33
+ | --- | --- | --- |
34
+ | [retry-exponential-backoff.yaml](retry-exponential-backoff.yaml) | The five fields, and the 1s, 2s, 4s they compute | `failed` |
35
+ | [retry-with-jitter.yaml](retry-with-jitter.yaml) | Why four items that failed together must not retry together | `completed_with_errors` |
36
+ | [retry-budget-exhausted.yaml](retry-budget-exhausted.yaml) | A budget spent in full on a hopeless failure | `failed` |
37
+ | [retry-only-transient.yaml](retry-only-transient.yaml) | The same policy on a 500 and a 404: three attempts and one | `completed_with_errors` |
38
+
39
+ ## Timeouts and deadlines
40
+
41
+ Three clocks that stop a step, and they are not interchangeable.
42
+
43
+ | File | What it shows | Ends as |
44
+ | --- | --- | --- |
45
+ | [timeout-fails-the-step.yaml](timeout-fails-the-step.yaml) | `timeout` bounds one block call, and expires as transient | `failed` |
46
+ | [timeout-skips-the-step.yaml](timeout-skips-the-step.yaml) | `on_timeout: skip` turns an expired deadline into a quiet day | `succeeded` |
47
+ | [deadline-on-a-sensor.yaml](deadline-on-a-sensor.yaml) | `poll`, `deadline` and the default `on_timeout: fail` | `failed` |
48
+ | [poll-cadence.yaml](poll-cadence.yaml) | The same wait polled two ways, with the overshoot measured | `succeeded` |
49
+
50
+ ## Fan-out
51
+
52
+ One step definition, many run items.
53
+
54
+ | File | What it shows | Ends as |
55
+ | --- | --- | --- |
56
+ | [fan-out-literal-list.yaml](fan-out-literal-list.yaml) | `for_each` written out in the document | `succeeded` |
57
+ | [fan-out-from-params.yaml](fan-out-from-params.yaml) | Width supplied by the caller, and the schema that guards it | `succeeded` |
58
+ | [fan-out-fail-fast.yaml](fan-out-fail-fast.yaml) | The default item policy: one bad element sinks the batch | `failed` |
59
+ | [fan-out-continue.yaml](fan-out-continue.yaml) | `items: continue`, and the failed item that is *absent* downstream | `completed_with_errors` |
60
+ | [fan-out-then-join.yaml](fan-out-then-join.yaml) | The join, which is just a step with no `for_each` of its own | `succeeded` |
61
+ | [fan-out-nested-objects.yaml](fan-out-nested-objects.yaml) | Elements that are objects, and `${item.limits.max_ms}` | `succeeded` |
62
+ | [fan-out-item-wise.yaml](fan-out-item-wise.yaml) | A second fan-out over the same grid, and the item it pairs with | `succeeded` |
63
+
64
+ ## Parameters
65
+
66
+ | File | What it shows | Ends as |
67
+ | --- | --- | --- |
68
+ | [params-every-type.yaml](params-every-type.yaml) | Every type and keyword, and why `format` needs a gate to assert | `succeeded` |
69
+ | [params-validation-refuses.yaml](params-validation-refuses.yaml) | What a bad run request is answered with, before a run exists | `succeeded` |
70
+
71
+ ## Triggers
72
+
73
+ What starts a run on its own. The declarations travel with the document; the operational state
74
+ stays on the instance.
75
+
76
+ | File | What it shows | Ends as |
77
+ | --- | --- | --- |
78
+ | [schedule-cron-timezone.yaml](schedule-cron-timezone.yaml) | Five columns, and the named zone that decides what they mean | `succeeded` |
79
+ | [schedule-interval.yaml](schedule-interval.yaml) | A cadence rather than a calendar, and the misfire grace | `succeeded` |
80
+ | [schedule-at-once.yaml](schedule-at-once.yaml) | One instant, anchored in the zone the schedule declares | `succeeded` |
81
+ | [schedule-window-half-open.yaml](schedule-window-half-open.yaml) | `${run.window.*}`, half-open; needs `--window` | `succeeded` |
82
+ | [webhook-mapping-nested-payload.yaml](webhook-mapping-nested-payload.yaml) | JSONPaths onto parameters, and the parameter left unreachable | `succeeded` |
83
+ | [webhook-signed.yaml](webhook-signed.yaml) | Outbound HMAC with `sign_with`, and where each secret lives | `succeeded` |
84
+
85
+ ## Concurrency
86
+
87
+ What a second trigger does while a run is still going. Each header spells that out; a single
88
+ local run has nothing to collide with, so the policies are exercised against a server.
89
+
90
+ | File | What a second trigger does | Ends as |
91
+ | --- | --- | --- |
92
+ | [concurrency-skip.yaml](concurrency-skip.yaml) | No run is created at all | `succeeded` |
93
+ | [concurrency-queue.yaml](concurrency-queue.yaml) | The run is created and held, with its parameters frozen | `succeeded` |
94
+ | [concurrency-replace.yaml](concurrency-replace.yaml) | The run in flight is cancelled, remotes told | `succeeded` |
95
+
96
+ ## Composition
97
+
98
+ `pipeline.run`, and the four questions a call answers. All of them need the child, which a
99
+ local run is handed with `--also-apply examples/patterns/pipeline-run-child.yaml`.
100
+
101
+ | File | What it shows | Ends as |
102
+ | --- | --- | --- |
103
+ | [pipeline-run-child.yaml](pipeline-run-child.yaml) | The called pipeline, which knows nothing about parents | `succeeded` |
104
+ | [pipeline-run-wait.yaml](pipeline-run-wait.yaml) | `wait: true`: a parked row, not a held worker | `succeeded` |
105
+ | [pipeline-run-fire-and-forget.yaml](pipeline-run-fire-and-forget.yaml) | `wait: false`: a fork, and the run id that is the whole handoff | `succeeded` |
106
+ | [pipeline-run-strict.yaml](pipeline-run-strict.yaml) | `strict`, and the third status it is about | `failed` |
107
+ | [pipeline-run-with-params.yaml](pipeline-run-with-params.yaml) | A fan-out of children, checked against the child's schema | `succeeded` |
108
+
109
+ ## The rest of the engine
110
+
111
+ | File | What it shows | Ends as |
112
+ | --- | --- | --- |
113
+ | [step-names-and-keys.yaml](step-names-and-keys.yaml) | The map key addresses; `name` describes and identifies nothing | `succeeded` |
114
+ | [outputs-inline-vs-storage.yaml](outputs-inline-vs-storage.yaml) | The 16KB inline threshold the engine decides on its own, against `storage.write` and `storage.read` as the document's own choice | `succeeded` |
115
+ | [references-cheat-sheet.yaml](references-cheat-sheet.yaml) | Every `${...}` form, each used once; needs `--window` | `succeeded` |
116
+ | [sensor-http-ready.yaml](sensor-http-ready.yaml) | A 503 as "not yet" rather than as an error | `succeeded` |
117
+ | [sensor-storage-exists.yaml](sensor-storage-exists.yaml) | A glob, a size floor, and a drop that actually lands | `succeeded` |
118
+ | [log-levels.yaml](log-levels.yaml) | `--log-level PATTERN=LEVEL`, a run setting rather than a document one | `succeeded` |
119
+ | [priority-layered.yaml](priority-layered.yaml) | `priority` on the document, on a schedule, and on one ad hoc run | `succeeded` |
120
+ | [connections-referenced-vs-carried.yaml](connections-referenced-vs-carried.yaml) | Named, carried, and absolute; needs `--connections` | `succeeded` |
121
+
122
+ ## Running them
123
+
124
+ Most take no flags at all:
125
+
126
+ ```bash
127
+ dg run --local examples/patterns/rule-one-failed.yaml
128
+ ```
129
+
130
+ Four need something handed to them, because what they demonstrate is exactly the thing a bare
131
+ local run does not have:
132
+
133
+ ```bash
134
+ dg run --local examples/patterns/references-cheat-sheet.yaml --window 2026-06-01..2026-06-02
135
+ dg run --local examples/patterns/schedule-window-half-open.yaml --window 2026-06-01..2026-06-02
136
+ dg run --local examples/patterns/connections-referenced-vs-carried.yaml \
137
+ --connections examples/connections.yaml
138
+ dg run --local examples/patterns/pipeline-run-wait.yaml \
139
+ --also-apply examples/patterns/pipeline-run-child.yaml
140
+ ```
141
+
142
+ Run the two without their flag once, on purpose: a run with no window refuses
143
+ `${run.window.start}` by name rather than resolving it to an empty string, which is the whole
144
+ argument for the reference language raising instead of defaulting.
@@ -0,0 +1,89 @@
1
+ # concurrency: queue -- a second trigger becomes a run that is created and held.
2
+ #
3
+ # WHAT A SECOND TRIGGER DOES HERE: the run IS created. It exists from the moment it is
4
+ # triggered, with its parameters and its window frozen as they were at that instant, and it
5
+ # simply does not advance -- no step is claimable while the slot is occupied. When the run
6
+ # ahead of it settles, the oldest held run is released and starts.
7
+ #
8
+ # That is the difference from skip, and it is the difference between losing work and delaying
9
+ # it. Use queue whenever a firing has work of its own that nothing later will redo: consuming
10
+ # a queue, appending a day's rows, advancing a cursor, sending a batch. The cost is that a
11
+ # backlog is real -- if the pipeline is consistently slower than its cadence, held runs
12
+ # accumulate, and that is a fact to look at rather than something the policy hides.
13
+ #
14
+ # THE PARAMETERS ARE FROZEN AT TRIGGER TIME, NOT AT RELEASE TIME. A run held for an hour still
15
+ # carries the parameters and the window it was created with, so an hourly increment released
16
+ # late covers the hour it was for, not the hour it happened to start in. That is the whole
17
+ # reason the run is created up front rather than the trigger being remembered.
18
+ #
19
+ # Everything about the ordering is FIFO: the oldest held run is the one released. A run
20
+ # cancelled while held simply never occupied the slot, and releasing does not double up.
21
+ #
22
+ # Hop by hop:
23
+ #
24
+ # claim_batch a sleep standing in for the work a firing owns -- the batch nothing else will
25
+ # pick up if this run never happens.
26
+ # commit records what the batch amounted to.
27
+ #
28
+ # EXPECT THIS RUN TO SUCCEED in about five seconds. A single local run has nothing to queue
29
+ # behind, so the policy is exercised against a server:
30
+ #
31
+ # dg run --local examples/patterns/concurrency-queue.yaml
32
+ # dg apply examples/patterns/concurrency-queue.yaml
33
+ # dg run concurrency-queue -p batch=1 &
34
+ # dg run concurrency-queue -p batch=2 # created, and held
35
+ # dg runs list concurrency-queue # two runs; the second starts when the first settles
36
+
37
+ format: dirigent/v1
38
+ kind: pipeline
39
+ code: concurrency-queue
40
+ name: A second run that waits its turn
41
+ description: |
42
+ `concurrency: queue` creates the second run and holds it: no step advances until the slot
43
+ is free, and then the oldest held run is released.
44
+
45
+ Its parameters and window are frozen at trigger time, so an increment released late still
46
+ covers the interval it was created for.
47
+
48
+ tags: [patterns, sensor, transform, concurrency]
49
+
50
+ # The one line this file is about.
51
+ concurrency: queue
52
+
53
+ requires:
54
+ blocks:
55
+ - time.sleep
56
+ - transform.jq
57
+
58
+ params:
59
+ type: object
60
+ properties:
61
+ batch:
62
+ type: integer
63
+ description: Which batch this firing owns; nothing else will pick it up.
64
+ default: 1
65
+ minimum: 1
66
+ seconds:
67
+ type: integer
68
+ description: How long the batch takes; long enough to queue a second run by hand.
69
+ default: 4
70
+ minimum: 1
71
+ maximum: 120
72
+
73
+ steps:
74
+ claim_batch:
75
+ block: time.sleep
76
+ poll: 1s
77
+ config:
78
+ for: "${params.seconds}s"
79
+
80
+ commit:
81
+ block: transform.jq
82
+ depends_on: [claim_batch]
83
+ # An append rather than a rebuild, which is what makes queue the right policy: a firing
84
+ # that never ran is a batch nobody ever committed.
85
+ config:
86
+ input:
87
+ batch: "${params.batch}"
88
+ program: |
89
+ {committed: .batch, incremental: true}
@@ -0,0 +1,92 @@
1
+ # concurrency: replace -- the newest trigger wins, and the run in flight is cancelled.
2
+ #
3
+ # WHAT A SECOND TRIGGER DOES HERE: every run currently in flight is CANCELLED, with the reason
4
+ # "replaced by a newer run", and the new run starts. Cancellation is not a suggestion -- what
5
+ # has not started is stopped, and an attempt holding a remote handle has its remote told, so a
6
+ # job submitted to something else is not left running behind a cancelled run.
7
+ #
8
+ # This is the "only the latest matters" policy, and it is the one to be most careful with,
9
+ # because it is the only one that destroys work already done. It fits a preview build, a
10
+ # dashboard refresh triggered by every commit, a re-render of a view where an older answer has
11
+ # no value the moment a newer request exists. It does not fit anything that writes: a load
12
+ # cancelled halfway is a load somebody has to reason about.
13
+ #
14
+ # THE ORDER OF OPERATIONS MATTERS AND IS WORTH KNOWING. A fan-out is expanded BEFORE the
15
+ # policy decides anything, so a document whose expansion would be refused is refused before
16
+ # any cancelling happens -- a refusal after the cancel would leave the old run dead and the
17
+ # new one non-existent.
18
+ #
19
+ # A cancelled run frees the slot exactly as a finished one does, and it settles as cancelled,
20
+ # which is neither succeeded nor failed. It stays in the run list with its attempts and logs:
21
+ # replaced is a thing to be able to see, not a thing to erase.
22
+ #
23
+ # Hop by hop:
24
+ #
25
+ # render a sleep standing in for a build whose older answer stops mattering the instant a
26
+ # newer request arrives.
27
+ # publish swaps the freshly rendered output in. It only ever runs for the newest request,
28
+ # which is the property this policy buys.
29
+ #
30
+ # EXPECT THIS RUN TO SUCCEED in about five seconds. A single local run has nothing to replace,
31
+ # so the policy is exercised against a server -- watch the first run settle as cancelled:
32
+ #
33
+ # dg run --local examples/patterns/concurrency-replace.yaml
34
+ # dg apply examples/patterns/concurrency-replace.yaml
35
+ # dg run concurrency-replace -p revision=abc123 &
36
+ # dg run concurrency-replace -p revision=def456 # the first is cancelled, this one starts
37
+ # dg runs list concurrency-replace # one cancelled, one succeeded
38
+
39
+ format: dirigent/v1
40
+ kind: pipeline
41
+ code: concurrency-replace
42
+ name: The newest trigger wins
43
+ description: |
44
+ `concurrency: replace` cancels every run in flight, telling any remote about the attempts
45
+ that had one, and starts the new run.
46
+
47
+ The only policy that destroys work already done. Right for a preview render whose older
48
+ answer is worthless; wrong for anything that writes.
49
+
50
+ tags: [patterns, sensor, transform, concurrency]
51
+
52
+ # The one line this file is about.
53
+ concurrency: replace
54
+
55
+ requires:
56
+ blocks:
57
+ - time.sleep
58
+ - transform.jq
59
+
60
+ params:
61
+ type: object
62
+ properties:
63
+ revision:
64
+ type: string
65
+ description: Which revision is being rendered; only the newest one matters.
66
+ default: abc123
67
+ minLength: 1
68
+ maxLength: 64
69
+ seconds:
70
+ type: integer
71
+ description: How long the render takes; long enough to replace it by hand.
72
+ default: 4
73
+ minimum: 1
74
+ maximum: 120
75
+
76
+ steps:
77
+ render:
78
+ block: time.sleep
79
+ poll: 1s
80
+ config:
81
+ for: "${params.seconds}s"
82
+
83
+ publish:
84
+ block: transform.jq
85
+ depends_on: [render]
86
+ # Nothing above this step writes anywhere, which is what makes cancelling it safe. A
87
+ # pipeline whose first step wrote to a real system would want queue and not this.
88
+ config:
89
+ input:
90
+ revision: "${params.revision}"
91
+ program: |
92
+ {published: .revision, superseded_anything_older: true}