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,134 @@
1
+ # storage.exists: waiting for an object, with a glob, a size floor, and a drop that arrives.
2
+ #
3
+ # deadline-on-a-sensor.yaml is this sensor missing its deadline. This one is the sensor doing
4
+ # its job: something writes the object while the run is going, the sensor sees it, and the
5
+ # pipeline carries on. Everything below is offline apart from one echo call, and the drop is
6
+ # written by the run itself so there is nothing to arrange.
7
+ #
8
+ # THE TWO CONFIG FIELDS, and both earn their place:
9
+ #
10
+ # uri may contain a GLOB in its final segments. Waiting for
11
+ # drops/2026-01-01/*.json is how a pipeline waits for "whatever the producer
12
+ # called it today" instead of demanding a name nobody agreed to. The sensor's
13
+ # output says which object actually matched, which is why every step downstream
14
+ # reads ${steps.<sensor>.output.uri} rather than rebuilding the pattern.
15
+ # min_size ignores an object until it is at least this large. A producer uploading a large
16
+ # file makes the key visible before the bytes are all there, so without a floor
17
+ # the sensor fires on a half-written object and the load reads a truncated file.
18
+ # This is the single most common way a drop-watching pipeline goes wrong.
19
+ #
20
+ # The output carries uri, size and modified_at -- what landed, how big it is, and when it was
21
+ # last written -- which is everything a downstream step needs to decide whether to trust it.
22
+ #
23
+ # Hop by hop:
24
+ #
25
+ # producer fetches the payload the drop is made of. It stands in for the upstream system.
26
+ # drop writes that payload to a URI under this run's scratch, which is the moment the
27
+ # sensor is waiting for: storage.write is the only way a value leaves a run.
28
+ # dropped the sensor. It matches on a glob, insists on a minimum size, and fires as soon
29
+ # as the object is there.
30
+ # archive copies what landed to its resting place, reading the sensor's answer rather than
31
+ # the pattern.
32
+ # receipt reports what was seen: the object, its size, and when it was written.
33
+ #
34
+ # WHERE A URI MAY POINT. ${run.scratch} is this run's own directory under the instance's
35
+ # artifact root, and file:// URIs are refused outside that root -- a stored pipeline cannot be
36
+ # turned into an arbitrary-file read. A real drop is an s3:// URI, or a path under that root;
37
+ # never an absolute path typed into a document.
38
+ #
39
+ # EXPECT THIS RUN TO SUCCEED in about two seconds, with the sensor firing on its first poke.
40
+ #
41
+ # To change it: raise -p min_size and the object is never big enough, so the sensor misses its
42
+ # deadline and the run fails -- which is the same shape deadline-on-a-sensor.yaml shows
43
+ # deliberately.
44
+ #
45
+ # dg run --local examples/patterns/sensor-storage-exists.yaml
46
+ # dg run --local examples/patterns/sensor-storage-exists.yaml -p min_size=10mb # fails at the deadline
47
+
48
+ format: dirigent/v1
49
+ kind: pipeline
50
+ code: sensor-storage-exists
51
+ name: Waiting for a drop
52
+ description: |
53
+ `storage.exists` waits for an object at a URI, which may end in a **glob**, and ignores it
54
+ until it is at least `min_size` -- the guard against opening a file a producer is still
55
+ uploading.
56
+
57
+ Its output says which object matched, how big it is and when it was written, so downstream
58
+ reads the answer rather than rebuilding the pattern.
59
+
60
+ tags: [patterns, http, sensor, storage, transform]
61
+
62
+ requires:
63
+ blocks:
64
+ - http.request
65
+ - storage.write
66
+ - storage.exists
67
+ - storage.copy
68
+ - transform.jq
69
+
70
+ params:
71
+ type: object
72
+ properties:
73
+ day:
74
+ type: string
75
+ format: date
76
+ description: The day the drop belongs to; it is a directory in the URI.
77
+ default: "2026-01-01"
78
+ min_size:
79
+ type: string
80
+ description: The floor the object has to clear. A humane size, so 1kb and 10mb both read as sizes.
81
+ default: 100b
82
+
83
+ steps:
84
+ producer:
85
+ block: http.request
86
+ # Stands in for the upstream system: it only fetches, and what it fetches is a value in
87
+ # its output like any other.
88
+ config:
89
+ url: https://postman-echo.com/get
90
+ query:
91
+ day: "${params.day}"
92
+
93
+ drop:
94
+ block: storage.write
95
+ depends_on: [producer]
96
+ # The write is what the sensor below is waiting for. The producer names the file, which
97
+ # is why the sensor matches a pattern rather than this exact target.
98
+ config:
99
+ target: "${run.scratch}/drops/${params.day}/export-0001.json"
100
+ value: "${steps.producer.output.body}"
101
+
102
+ dropped:
103
+ block: storage.exists
104
+ depends_on: [drop]
105
+ poll: 1s
106
+ deadline: 15s
107
+ config:
108
+ # A glob in the final segment: the producer names the file, and the pipeline does not
109
+ # have to know what it will be called.
110
+ uri: "${run.scratch}/drops/${params.day}/*.json"
111
+ # A configured size is humane and carries no _bytes suffix: the type says the unit.
112
+ min_size: "${params.min_size}"
113
+
114
+ archive:
115
+ block: storage.copy
116
+ depends_on: [dropped]
117
+ config:
118
+ # The sensor's answer, not the pattern. A glob may have matched anything; only the
119
+ # sensor knows what it actually matched.
120
+ source: "${steps.dropped.output.uri}"
121
+ target: "${run.scratch}/archive/${params.day}.json"
122
+
123
+ receipt:
124
+ block: transform.jq
125
+ depends_on: [archive]
126
+ config:
127
+ input:
128
+ landed: "${steps.dropped.output.uri}"
129
+ size: "${steps.dropped.output.size}"
130
+ modified_at: "${steps.dropped.output.modified_at}"
131
+ archived: "${steps.archive.output.target}"
132
+ copied_bytes: "${steps.archive.output.bytes_copied}"
133
+ program: |
134
+ {landed, size, modified_at, archived, copied_bytes}
@@ -0,0 +1,99 @@
1
+ # The map key is the identity; name is a title nothing may ever reference.
2
+ #
3
+ # Every addressable thing in dirigent carries the same quartet, and each field has exactly one
4
+ # job:
5
+ #
6
+ # id the uuid a machine holds. Never written in a document.
7
+ # code the addressable key: constrained, unique, and what appears in a URL, in a
8
+ # document, and in every reference. A step's map key IS its code.
9
+ # name an optional human title. Free-form, with no identity semantics at all --
10
+ # nothing may ever be referenced by it, and two things may share one.
11
+ # description long-form and markdown-capable; the body a screen renders. A STEP carries no
12
+ # description -- only name and its key -- because a step's explanation is the
13
+ # comment above it, which travels in the document a person reads.
14
+ #
15
+ # So a step is referenced by its map key and by nothing else: ${steps.load_batch.output.status}
16
+ # names load_batch, and renaming the STEP NAME below to anything at all leaves every reference
17
+ # in this document working. Renaming the KEY breaks them, and breaks them at apply time, which
18
+ # is the trade -- an identifier that is checkable is an identifier that cannot quietly drift.
19
+ #
20
+ # Two steps in this file deliberately share a name. That is legal, because a name identifies
21
+ # nothing; the keys tell them apart. A screen shows the name where there is one and the key
22
+ # otherwise, with the key always on the page in mono and never drawn twice.
23
+ #
24
+ # The same quartet, in the same shape, everywhere: the pipeline's code is its filename here
25
+ # (the corpus test insists on it), a schedule's code addresses it, a webhook's code addresses
26
+ # it, and a connection's code is what a document names instead of carrying a credential.
27
+ #
28
+ # Hop by hop:
29
+ #
30
+ # load_batch a key in snake_case, and a name that reads like a sentence.
31
+ # verify_rows depends on the KEY above, not on its name, and reads its output the same way.
32
+ # publish carries the same name as verify_rows, which nothing anywhere minds.
33
+ #
34
+ # EXPECT THIS RUN TO SUCCEED in about a second, with no network and no allowlist.
35
+ #
36
+ # To change it: edit any name below and re-run -- everything still works. Edit a key and the
37
+ # document stops validating, with the dangling reference named.
38
+ #
39
+ # dg run --local examples/patterns/step-names-and-keys.yaml
40
+
41
+ format: dirigent/v1
42
+ kind: pipeline
43
+ code: step-names-and-keys
44
+ name: Keys address, names describe
45
+ description: |
46
+ A step's map **key** is its code: the only thing anything references, and what
47
+ `${steps.<key>.output...}` names.
48
+
49
+ `name` is a free-form title with no identity semantics -- two steps may share one, and
50
+ renaming it breaks nothing.
51
+
52
+ tags: [patterns, transform, conventions, references]
53
+
54
+ requires:
55
+ blocks:
56
+ - transform.jq
57
+
58
+ params:
59
+ type: object
60
+ properties:
61
+ batch:
62
+ type: string
63
+ description: Which batch this run covers.
64
+ default: 2026-01-01-cases
65
+
66
+ steps:
67
+ load_batch:
68
+ # A title, for a person reading a run. Change it freely: nothing in this document, in the
69
+ # API, or in a URL refers to it.
70
+ name: Load the nightly batch
71
+ block: transform.jq
72
+ config:
73
+ input:
74
+ batch: "${params.batch}"
75
+ program: |
76
+ {batch, rows: 4120}
77
+
78
+ verify_rows:
79
+ name: Check the batch
80
+ block: transform.jq
81
+ # The key, never the name. A dependency on "Load the nightly batch" is not expressible,
82
+ # which is the point.
83
+ depends_on: [load_batch]
84
+ config:
85
+ # Same rule in a reference: steps.<key>.output.<field>.
86
+ input: "${steps.load_batch.output.value}"
87
+ program: |
88
+ {batch, rows, verified: (.rows > 0)}
89
+
90
+ publish:
91
+ # Deliberately the same name as the step above. Two rows may share a name because a name
92
+ # identifies nothing; publish and verify_rows are still two different steps.
93
+ name: Check the batch
94
+ block: transform.jq
95
+ depends_on: [verify_rows]
96
+ config:
97
+ input: "${steps.verify_rows.output.value}"
98
+ program: |
99
+ {published: .batch, rows}
@@ -0,0 +1,94 @@
1
+ # timeout: how long ONE block call may take, and what an expired one is classified as.
2
+ #
3
+ # Three different clocks can stop a step, and they answer three different questions:
4
+ #
5
+ # timeout (a step field) how long one block call may take. This file.
6
+ # deadline (a step field) how long a step may keep WAITING on a sensor or a
7
+ # remote job. deadline-on-a-sensor.yaml.
8
+ # timeout (block config) the block's own limit on its own work, such as the
9
+ # HTTP client's socket timeout.
10
+ #
11
+ # The two on slow_read below are set deliberately far apart: the endpoint is asked to take ten
12
+ # seconds, the HTTP client would allow thirty, and the engine allows two. So the engine is
13
+ # plainly the clock that fires, and the attempt says so.
14
+ #
15
+ # An expired timeout is a TRANSIENT failure, which matters more than it looks: a retry budget
16
+ # would be spent on it, three attempts against a call that is always too slow costing three
17
+ # full timeouts of waiting. There is no retry policy here for exactly that reason -- a
18
+ # deadline that cannot be met is not a flake. And note what on_timeout does NOT do: it governs
19
+ # an expired deadline, never an expired timeout, so there is no spelling of this step that
20
+ # skips instead of failing. timeout-skips-the-step.yaml is where that distinction lives.
21
+ #
22
+ # Hop by hop:
23
+ #
24
+ # start a local value, so the run has a step that plainly succeeded to compare against.
25
+ # slow_read /delay/10 against a two-second budget. It loses, in about two seconds.
26
+ # parse the default all_success edge, so it is skipped.
27
+ #
28
+ # EXPECT THIS RUN TO FAIL, in about two seconds, with slow_read failed and parse skipped.
29
+ # It fails ON TIME, not on work: the endpoint was healthy and would have answered.
30
+ #
31
+ # To change it: -p seconds=1 finishes inside the budget and the run goes green. Widen the
32
+ # step's timeout past ten and the same happens. Narrow the block's own timeout below two and
33
+ # the HTTP client gives up first -- same red step, different reason, and the attempt's message
34
+ # says which.
35
+ #
36
+ # dg run --local examples/patterns/timeout-fails-the-step.yaml # fails in ~2s
37
+ # dg run --local examples/patterns/timeout-fails-the-step.yaml -p seconds=1 # succeeds
38
+
39
+ format: dirigent/v1
40
+ kind: pipeline
41
+ code: timeout-fails-the-step
42
+ name: A step that runs out of time
43
+ description: |
44
+ `timeout` bounds one block call. An expired one is a **transient** failure, so a retry
45
+ budget would be spent on it -- which is why this step has none.
46
+
47
+ As written a ten-second endpoint is given two seconds, the step fails in about two, and
48
+ the run reports `failed`.
49
+
50
+ tags: [patterns, http, transform, failure, timeout]
51
+
52
+ requires:
53
+ blocks:
54
+ - http.request
55
+ - value.const
56
+
57
+ params:
58
+ type: object
59
+ properties:
60
+ seconds:
61
+ type: integer
62
+ description: How long the endpoint is asked to take. Anything above the two-second budget loses.
63
+ default: 10
64
+ minimum: 0
65
+ maximum: 30
66
+
67
+ steps:
68
+ start:
69
+ block: value.const
70
+ config:
71
+ value:
72
+ note: everything above the failing step is ordinary
73
+
74
+ slow_read:
75
+ block: http.request
76
+ depends_on: [start]
77
+ # The budget the engine holds the call to. It is the shortest of the three clocks in this
78
+ # file, which is what makes it the one that fires.
79
+ timeout: 2s
80
+ config:
81
+ url: "https://postman-echo.com/delay/${params.seconds}"
82
+ method: GET
83
+ # The block's own clock, left generous so it is plainly not the one that stops this.
84
+ timeout: 30s
85
+
86
+ parse:
87
+ block: transform.jq
88
+ depends_on: [slow_read]
89
+ # Skipped. An abandoned attempt is a failure like any other as far as the edge is
90
+ # concerned; there is no separate "timed out" outcome for a rule to branch on.
91
+ config:
92
+ input: "${steps.slow_read.output.body}"
93
+ program: |
94
+ {delayed: .}
@@ -0,0 +1,102 @@
1
+ # on_timeout: skip -- the wait that ran out and did not make the run red.
2
+ #
3
+ # WHICH CLOCK on_timeout GOVERNS IS THE WHOLE POINT OF THIS FILE. It governs an expired
4
+ # DEADLINE, which is how long a step may keep waiting. It has nothing to say about an expired
5
+ # `timeout`, which bounds one block call and always fails the step as a transient failure --
6
+ # timeout-fails-the-step.yaml is that one, and there is no spelling of it that skips.
7
+ #
8
+ # So this is the pair to read together. The same waiting step, one word apart:
9
+ #
10
+ # on_timeout: fail (the default) the deadline passing is a broken pipeline.
11
+ # deadline-on-a-sensor.yaml.
12
+ # on_timeout: skip the deadline passing is a quiet day. This file.
13
+ #
14
+ # skip is the right answer whenever "it never showed up" is an ordinary outcome: no export
15
+ # was published today, no drop landed, the upstream had nothing to say. Failing there trains
16
+ # people to ignore red runs, which is worse than the day with no data.
17
+ #
18
+ # Hop by hop:
19
+ #
20
+ # upstream_ready http.ready is a sensor: each poke is one cheap GET, "not yet" is the
21
+ # expected answer, and the run parks between pokes rather than holding a
22
+ # worker. It pokes /status/503, which never answers 2xx, so it never becomes
23
+ # ready and the six-second deadline expires.
24
+ # ingest the default all_success edge, and a skipped prerequisite does not satisfy
25
+ # it, so the skip carries down the branch.
26
+ # close_out all_done, so the run still reports what it did with the day.
27
+ #
28
+ # EXPECT THIS RUN TO SUCCEED, in about six seconds, with upstream_ready and ingest both
29
+ # skipped and close_out green. A run that did nothing is not a run that went wrong.
30
+ #
31
+ # To change it: -p status=200 makes the endpoint ready on the first poke and the whole branch
32
+ # runs. Drop the on_timeout line and the identical run fails instead.
33
+ #
34
+ # dg run --local examples/patterns/timeout-skips-the-step.yaml # succeeds, having skipped
35
+ # dg run --local examples/patterns/timeout-skips-the-step.yaml -p status=200 # succeeds, having worked
36
+
37
+ format: dirigent/v1
38
+ kind: pipeline
39
+ code: timeout-skips-the-step
40
+ name: A wait that skips instead of failing
41
+ description: |
42
+ `on_timeout: skip` turns an expired **deadline** into a skipped step rather than a failed
43
+ one, and the skip carries down the branch through the default `all_success` edges.
44
+
45
+ It governs the deadline only: an expired `timeout` always fails. As written the sensor
46
+ never sees a ready endpoint, everything below it skips, and the run reports `succeeded`.
47
+
48
+ tags: [patterns, http, sensor, timeout]
49
+
50
+ requires:
51
+ blocks:
52
+ - http.ready
53
+ - http.request
54
+ - value.const
55
+
56
+ params:
57
+ type: object
58
+ properties:
59
+ status:
60
+ type: integer
61
+ description: What the readiness endpoint answers; 503 is never ready, 200 is ready at once.
62
+ default: 503
63
+ enum: [200, 503]
64
+
65
+ steps:
66
+ upstream_ready:
67
+ block: http.ready
68
+ # Two seconds between pokes, so three of them fit inside the deadline and the parking
69
+ # between them is visible in the log. A real readiness check on an hourly export uses
70
+ # minutes here, because each poke costs a scheduled row and nothing else.
71
+ poll: 2s
72
+ # Short so the example finishes while you watch it. This is the clock on_timeout is about.
73
+ deadline: 6s
74
+ # The one line this file is about.
75
+ on_timeout: skip
76
+ config:
77
+ # A sensor has no method: it looks, and looking is a GET. Readiness means a 2xx unless
78
+ # expect_status says otherwise, so a 503 is "not yet" rather than an error -- a sensor
79
+ # observing an unready service has observed correctly.
80
+ url: "https://postman-echo.com/status/${params.status}"
81
+
82
+ ingest:
83
+ block: http.request
84
+ depends_on: [upstream_ready]
85
+ # No rule written, so all_success, which a skipped prerequisite does not satisfy. That
86
+ # propagation is what makes on_timeout: skip a branch-level decision rather than a
87
+ # step-level one.
88
+ config:
89
+ url: https://postman-echo.com/post
90
+ method: POST
91
+ body:
92
+ ingested_after_status: "${steps.upstream_ready.output.status}"
93
+
94
+ close_out:
95
+ block: value.const
96
+ depends_on: [ingest]
97
+ # all_done, so the run has an outcome either way: a skipped day is a fact worth recording,
98
+ # not an absence.
99
+ rule: all_done
100
+ config:
101
+ value:
102
+ note: nothing to ingest is a normal day
@@ -0,0 +1,125 @@
1
+ # An inbound webhook, and the strict mapping that is the whole of its interface.
2
+ #
3
+ # A webhook declares one thing: which JSONPath in the delivered payload becomes which
4
+ # parameter. That mapping is the entire attack surface, and it is strict in both directions:
5
+ #
6
+ # * Anything the payload carries that is not named here is IGNORED. A caller who adds a
7
+ # field, or fifty, changes nothing about the run.
8
+ # * A parameter the mapping does not name cannot be reached at all, whatever is POSTed. So
9
+ # environment below is unreachable from outside and keeps its default -- which is how a
10
+ # webhook is exposed without letting the internet choose which environment to write to.
11
+ # * What is mapped is validated against params like any other run's parameters. A payload
12
+ # with a batch_size of 10000000 is refused by the schema, not by a step.
13
+ #
14
+ # The paths are JSONPath into the delivered body, so a nested payload needs no unwrapping step:
15
+ # $.published.date reaches two levels down, and $.source.system.name three. Reshaping the
16
+ # payload inside the pipeline would be doing at run time what the mapping already does at
17
+ # intake, one layer later and with worse errors.
18
+ #
19
+ # WHAT STAYS ON THE INSTANCE. The token is minted server-side and shown exactly once. It is
20
+ # not in this file, it will not be in an export, and there is nowhere in dirigent/v1 to write
21
+ # one. The same is true of whether the endpoint is enabled and of the delivery history: those
22
+ # are facts about one instance, so an apply never touches them.
23
+ #
24
+ # Hop by hop:
25
+ #
26
+ # ingest shapes the mapped parameters into the record a loader would be handed.
27
+ # receipt reads it back, standing in for the work.
28
+ #
29
+ # EXPECT THIS RUN TO SUCCEED in about a second, on the pipeline's defaults -- the webhook
30
+ # accepts deliveries only on an instance the document has been applied to.
31
+ #
32
+ # dg run --local examples/patterns/webhook-mapping-nested-payload.yaml
33
+ # dg apply examples/patterns/webhook-mapping-nested-payload.yaml
34
+ # dg webhook list webhook-mapping-nested-payload
35
+ # dg webhook rotate-token webhook-mapping-nested-payload upstream-publish # shows a token once
36
+ # curl -X POST "$DG_URL/hooks/$TOKEN" -H 'content-type: application/json' -d '{
37
+ # "published": {"date": "2026-01-01", "dataset": {"code": "cases"}},
38
+ # "source": {"system": {"name": "warehouse"}},
39
+ # "counts": {"rows": 4120},
40
+ # "anything_else": "ignored"
41
+ # }'
42
+ # dg webhook deliveries webhook-mapping-nested-payload upstream-publish
43
+
44
+ format: dirigent/v1
45
+ kind: pipeline
46
+ code: webhook-mapping-nested-payload
47
+ name: A webhook mapped from a nested payload
48
+ description: |
49
+ `params_from_payload` maps JSONPaths in the delivered body onto parameters, and that
50
+ mapping is the whole interface: unmapped fields are ignored, unmapped parameters are
51
+ unreachable, and what is mapped is validated like any other run's parameters.
52
+
53
+ The token, the enabled flag and the delivery history stay on the instance; none of them
54
+ belongs in git.
55
+
56
+ tags: [patterns, transform, webhook]
57
+
58
+ requires:
59
+ blocks:
60
+ - transform.jq
61
+
62
+ params:
63
+ type: object
64
+ properties:
65
+ day:
66
+ type: string
67
+ format: date
68
+ description: The day the upstream published; mapped from the payload.
69
+ default: "2026-01-01"
70
+ dataset:
71
+ type: string
72
+ description: Which dataset was published; mapped from two levels down.
73
+ default: cases
74
+ enum: [cases, climate, population]
75
+ source:
76
+ type: string
77
+ description: Which upstream system published it; mapped from three levels down.
78
+ default: warehouse
79
+ minLength: 1
80
+ maxLength: 64
81
+ rows:
82
+ type: integer
83
+ description: How many rows the upstream says it published; bounded, so a delivery cannot claim anything.
84
+ default: 0
85
+ minimum: 0
86
+ maximum: 10000000
87
+ environment:
88
+ type: string
89
+ description: Deliberately absent from the mapping, so no delivery can choose it.
90
+ default: staging
91
+ enum: [staging, production]
92
+
93
+ steps:
94
+ ingest:
95
+ block: transform.jq
96
+ config:
97
+ input:
98
+ day: "${params.day}"
99
+ dataset: "${params.dataset}"
100
+ source: "${params.source}"
101
+ rows: "${params.rows}"
102
+ environment: "${params.environment}"
103
+ program: |
104
+ {day, dataset, source, rows, environment}
105
+
106
+ receipt:
107
+ block: transform.jq
108
+ depends_on: [ingest]
109
+ config:
110
+ input: "${steps.ingest.output.value}"
111
+ program: |
112
+ {ingested: "\(.dataset) for \(.day) from \(.source)", rows, environment}
113
+
114
+ triggers:
115
+ webhooks:
116
+ - code: upstream-publish
117
+ name: Upstream publish notification
118
+ description: Fired by the warehouse when a dataset finishes publishing.
119
+ params_from_payload:
120
+ # One line per parameter, each a JSONPath into the delivered body. Depth costs
121
+ # nothing; a field the mapping does not name is not reachable at any depth.
122
+ day: "$.published.date"
123
+ dataset: "$.published.dataset.code"
124
+ source: "$.source.system.name"
125
+ rows: "$.counts.rows"