···7474 "description": "Records to fetch from PDS before executing actions. Fetched data is available as named variables in action templates.",
7575 "maxLength": 5,
7676 "items": {
7777- "type": "ref",
7878- "ref": "#fetchStep"
7777+ "type": "union",
7878+ "refs": ["#fetchStep", "#fetchSearchStep"]
7979 }
8080 },
8181 "active": {
···150150 "description": "AT URI template, e.g. '{{event.commit.record.subject}}'.",
151151 "maxLength": 2048
152152 },
153153+ "conditions": {
154154+ "type": "array",
155155+ "description": "Conditions evaluated against this fetch's result after it resolves. All must pass or the automation is skipped. Field paths are resolved relative to the fetch's entry (e.g. 'found', 'record.subject').",
156156+ "maxLength": 10,
157157+ "items": {
158158+ "type": "ref",
159159+ "ref": "#condition"
160160+ }
161161+ },
153162 "comment": {
154163 "type": "string",
155164 "description": "Optional user note about this fetch step.",
···157166 }
158167 }
159168 },
169169+ "fetchSearchStep": {
170170+ "type": "object",
171171+ "description": "Search for a record on a PDS by matching fields. Useful for checking if a record already exists (e.g. duplicate-prevention before creating).",
172172+ "required": ["name", "repo", "collection", "where"],
173173+ "properties": {
174174+ "name": {
175175+ "type": "string",
176176+ "description": "Variable name for use in templates, e.g. 'existingMirror'. Check `.found` or use the `exists`/`not-exists` condition operator to test whether the search matched.",
177177+ "maxLength": 64
178178+ },
179179+ "repo": {
180180+ "type": "string",
181181+ "description": "DID template for the repo to search. Supports {{self}}, {{event.*}}, and upstream fetch references.",
182182+ "maxLength": 256
183183+ },
184184+ "collection": {
185185+ "type": "string",
186186+ "description": "NSID of the collection to search.",
187187+ "maxLength": 256
188188+ },
189189+ "where": {
190190+ "type": "array",
191191+ "description": "Field filters applied to each record. This version accepts a single clause; multi-clause support is planned.",
192192+ "minLength": 1,
193193+ "maxLength": 1,
194194+ "items": {
195195+ "type": "ref",
196196+ "ref": "#fetchSearchWhere"
197197+ }
198198+ },
199199+ "limit": {
200200+ "type": "integer",
201201+ "description": "Maximum number of matches to return. This version accepts 1 only; multi-match support is planned.",
202202+ "minimum": 1,
203203+ "maximum": 1,
204204+ "default": 1
205205+ },
206206+ "conditions": {
207207+ "type": "array",
208208+ "description": "Conditions evaluated against this fetch's result after it resolves. All must pass or the automation is skipped. Field paths are resolved relative to the fetch's entry (e.g. 'found', 'record.subject').",
209209+ "maxLength": 10,
210210+ "items": {
211211+ "type": "ref",
212212+ "ref": "#condition"
213213+ }
214214+ },
215215+ "comment": {
216216+ "type": "string",
217217+ "description": "Optional user note about this fetch step.",
218218+ "maxLength": 512
219219+ }
220220+ }
221221+ },
222222+ "fetchSearchWhere": {
223223+ "type": "object",
224224+ "description": "A single field filter for a search fetch step.",
225225+ "required": ["field", "value"],
226226+ "properties": {
227227+ "field": {
228228+ "type": "string",
229229+ "description": "Dot-path into each record, e.g. 'subject'.",
230230+ "maxLength": 256
231231+ },
232232+ "operator": {
233233+ "type": "string",
234234+ "description": "Comparison operator. Only 'eq' is currently supported.",
235235+ "knownValues": ["eq"],
236236+ "default": "eq",
237237+ "maxLength": 32
238238+ },
239239+ "value": {
240240+ "type": "string",
241241+ "description": "Value template to compare against. Supports {{self}}, {{event.*}}, and upstream fetch references.",
242242+ "maxLength": 1024
243243+ }
244244+ }
245245+ },
160246 "bskyPostAction": {
161247 "type": "object",
162248 "description": "Create a Bluesky post when a matching event occurs.",
···257343 },
258344 "condition": {
259345 "type": "object",
260260- "description": "A single filter condition on an event field.",
261261- "required": ["field", "value"],
346346+ "description": "A single filter condition. At the automation's top level, conditions run against the incoming event before any fetches. Attached to a fetch step, conditions run after that fetch resolves and are evaluated against the fetch's own result entry.",
347347+ "required": ["field"],
262348 "properties": {
263349 "field": {
264350 "type": "string",
265265- "description": "Dot-path to the event field, e.g. 'repo' or 'record.subject'.",
351351+ "description": "Dot-path to the field being checked. For top-level conditions: 'event.did' or a record field like 'subject'. For fetch-attached conditions: a path into the fetch's result, e.g. 'found', 'uri', or 'record.subject'.",
266352 "maxLength": 256
267353 },
268354 "operator": {
269355 "type": "string",
270270- "description": "Comparison operator.",
271271- "knownValues": ["eq", "startsWith", "endsWith", "contains"],
356356+ "description": "Comparison operator. 'exists' and 'not-exists' ignore `value`. When used on the bare 'found' field of a fetch-attached condition, they test the fetch's found flag directly.",
357357+ "knownValues": ["eq", "startsWith", "endsWith", "contains", "exists", "not-exists"],
272358 "default": "eq",
273359 "maxLength": 32
274360 },
275361 "value": {
276362 "type": "string",
277277- "description": "Value to compare against.",
363363+ "description": "Value to compare against. Ignored by the 'exists' and 'not-exists' operators.",
278364 "maxLength": 1024
279365 },
280366 "comment": {