blueprint_creation_guide.md
edited
1# Blueprint Creation Guide
2
3This guide explains how to create blueprints for the ifthisthenat automation system. Blueprints are automated workflows that respond to events on the AT Protocol network (including Bluesky) and can perform various actions.
4
5## Overview
6
7A blueprint consists of:
8
91. **Entry Node**: Defines what triggers the blueprint (usually `jetstream_entry` for AT Protocol events)
102. **Processing Nodes**: Transform, filter, or validate the data (e.g., `condition`, `transform`)
113. **Action Node**: Performs the final action (e.g., `publish_record`, `publish_webhook`)
12
13Blueprints are evaluated sequentially, with each node's output becoming the next node's input.
14
15## Jetstream Event Examples
16
17The system receives real-time events from the AT Protocol network via Jetstream. Here are examples of the event structures you'll work with:
18
19### Bluesky Feed Post Event
20
21```json
22{
23 "commit": {
24 "cid": "bafyreiga4g6vrd3lj557afdyec4xwld4gs2t3u47y4ybggvnc7i24udcnq",
25 "collection": "app.bsky.feed.post",
26 "operation": "create",
27 "record": {
28 "$type": "app.bsky.feed.post",
29 "createdAt": "2025-09-04T03:19:50.142Z",
30 "langs": [
31 "en"
32 ],
33 "text": "Who's interested in automation?"
34 },
35 "rev": "3lxy6sv2j5k2b",
36 "rkey": "3lxy6suve4c2y"
37 },
38 "did": "did:plc:tgudj2fjm77pzkuawquqhsxm",
39 "kind": "commit",
40 "time_us": 1756955990660025
41}
42```
43
44### Bluesky Feed Like Event
45
46```json
47{
48 "commit": {
49 "cid": "bafyreihus6whshnyleuzec4okl7gbweyxfsiacc5qhfnn5wqumpstklvey",
50 "collection": "app.bsky.feed.like",
51 "operation": "create",
52 "record": {
53 "$type": "app.bsky.feed.like",
54 "createdAt": "2025-09-04T03:20:36.439Z",
55 "subject": {
56 "cid": "bafyreiga4g6vrd3lj557afdyec4xwld4gs2t3u47y4ybggvnc7i24udcnq",
57 "uri": "at://did:plc:tgudj2fjm77pzkuawquqhsxm/app.bsky.feed.post/3lxy6suve4c2y"
58 }
59 },
60 "rev": "3lxy6ub5bok2b",
61 "rkey": "3lxy6ub42mk2b"
62 },
63 "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
64 "kind": "commit",
65 "time_us": 1756956036754875
66}
67```
68
69### Bluesky Feed Post Reply Event
70
71```json
72{
73 "commit": {
74 "cid": "bafyreif53mmglhsbqeuk6zftxh2kauz5l2jbnlavrvluobqdmm33f6uga4",
75 "collection": "app.bsky.feed.post",
76 "operation": "create",
77 "record": {
78 "$type": "app.bsky.feed.post",
79 "createdAt": "2025-09-04T03:26:18.985Z",
80 "langs": [
81 "en"
82 ],
83 "reply": {
84 "parent": {
85 "cid": "bafyreiga4g6vrd3lj557afdyec4xwld4gs2t3u47y4ybggvnc7i24udcnq",
86 "uri": "at://did:plc:tgudj2fjm77pzkuawquqhsxm/app.bsky.feed.post/3lxy6suve4c2y"
87 },
88 "root": {
89 "cid": "bafyreiga4g6vrd3lj557afdyec4xwld4gs2t3u47y4ybggvnc7i24udcnq",
90 "uri": "at://did:plc:tgudj2fjm77pzkuawquqhsxm/app.bsky.feed.post/3lxy6suve4c2y"
91 }
92 },
93 "text": "I know I sure am."
94 },
95 "rev": "3lxy76hutsk2b",
96 "rkey": "3lxy76hpwlc2y"
97 },
98 "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
99 "kind": "commit",
100 "time_us": 1756956379343219
101}
102```
103
104### Lexicon Community Calendar Event
105
106```json
107{
108 "commit": {
109 "cid": "bafyreihenqybbcny2al7t3pi5h5m25jmpqxwfwia5jvntcxh76rhh6wxpe",
110 "collection": "community.lexicon.calendar.event",
111 "operation": "create",
112 "record": {
113 "$type": "community.lexicon.calendar.event",
114 "createdAt": "2025-09-04T03:32:33.621Z",
115 "description": "DAYTON MUSIC FEST 2025 IS SEPTEMBER 5TH AT THE BRIGHTSIDE!\n\nSeptember 5, 2025\nDoors open at 7pm\nIllwin (hip-hop) at 8pm\nSocks (alternative) at 9:15pm\ncrabswithoutlegs (jazz-fusion) at 10:30pm",
116 "locations": [
117 {
118 "$type": "community.lexicon.location.address",
119 "country": "US",
120 "locality": "Dayton",
121 "name": "The Brightside",
122 "postalCode": "45402",
123 "region": "Ohio",
124 "street": "905 E 3rd St"
125 }
126 ],
127 "mode": "community.lexicon.calendar.event#inperson",
128 "name": "Dayton Music Fest",
129 "startsAt": "2025-09-05T21:30:00.000Z",
130 "status": "community.lexicon.calendar.event#scheduled",
131 "uris": [
132 {
133 "$type": "community.lexicon.calendar.event#uri",
134 "name": "thebrightsidedayton.com",
135 "uri": "https://www.thebrightsidedayton.com/event-details/dayton-music-fest-2"
136 }
137 ]
138 },
139 "rev": "3lxy7jnc6wc2b",
140 "rkey": "3lxy7jnbfjs2b"
141 },
142 "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
143 "kind": "commit",
144 "time_us": 1756956754949182
145}
146```
147
148### Lexicon Community Calendar RSVP Event
149
150```json
151{
152 "commit": {
153 "cid": "bafyreicpfvezre4cuyzvc4xsiri4wodhxworsvfpmkka6o3j3bzkmgdani",
154 "collection": "community.lexicon.calendar.rsvp",
155 "operation": "create",
156 "record": {
157 "$type": "community.lexicon.calendar.rsvp",
158 "createdAt": "2025-09-04T03:32:45.166Z",
159 "status": "community.lexicon.calendar.rsvp#going",
160 "subject": {
161 "cid": "bafyreihenqybbcny2al7t3pi5h5m25jmpqxwfwia5jvntcxh76rhh6wxpe",
162 "uri": "at://did:plc:cbkjy5n7bk3ax2wplmtjofq2/community.lexicon.calendar.event/3lxy7jnbfjs2b"
163 }
164 },
165 "rev": "3lxy7jy3rjc2b",
166 "rkey": "BF1P0SPNSZQPS"
167 },
168 "did": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
169 "kind": "commit",
170 "time_us": 1756956766029684
171}
172```
173
174## Node Configuration Examples
175
176### Jetstream Entry Nodes
177
178#### Match by Collection
179Triggers when any record in the specified collection is created:
180
181```json
182{
183 "node_type": "jetstream_entry",
184 "configuration": {
185 "collection": ["app.bsky.feed.post"]
186 },
187 "payload": true
188}
189```
190
191#### Match by Identity (DID)
192Triggers when a specific user performs any action:
193
194```json
195{
196 "node_type": "jetstream_entry",
197 "configuration": {
198 "did": ["did:plc:tgudj2fjm77pzkuawquqhsxm"]
199 },
200 "payload": true
201}
202```
203
204#### Match Both Collection and Identity
205Triggers when specific users perform actions in specific collections:
206
207```json
208{
209 "node_type": "jetstream_entry",
210 "configuration": {
211 "did": ["did:plc:tgudj2fjm77pzkuawquqhsxm", "did:plc:cbkjy5n7bk3ax2wplmtjofq2"],
212 "collection": ["app.bsky.feed.post", "app.bsky.feed.like"]
213 },
214 "payload": true
215}
216```
217
218#### Match Posts with Specific Text Content
219Triggers when posts contain specific keywords:
220
221```json
222{
223 "node_type": "jetstream_entry",
224 "configuration": {
225 "collection": ["app.bsky.feed.post"]
226 },
227 "payload": {
228 "and": [
229 {"==": [{"val": ["commit", "operation"]}, "create"]},
230 {"exists": ["commit", "record", "text"]},
231 {"or": [
232 {"starts_with": [{"val": ["commit", "record", "text"]}, "#automation"]},
233 {"ends_with": [{"val": ["commit", "record", "text"]}, "#ifthisthenat"]}
234 ]}
235 ]
236 }
237}
238```
239
240#### Match Replies to Specific Post
241Triggers when someone replies to a specific post:
242
243```json
244{
245 "node_type": "jetstream_entry",
246 "configuration": {
247 "collection": ["app.bsky.feed.post"]
248 },
249 "payload": {
250 "and": [
251 {"exists": ["commit", "record", "reply"]},
252 {"==": [
253 {"val": ["commit", "record", "reply", "parent", "uri"]},
254 "at://did:plc:tgudj2fjm77pzkuawquqhsxm/app.bsky.feed.post/3lxy6suve4c2y"
255 ]}
256 ]
257 }
258}
259```
260
261#### Match Posts with Minimum Length
262Triggers on posts that meet minimum text length requirements:
263
264```json
265{
266 "node_type": "jetstream_entry",
267 "configuration": {
268 "collection": ["app.bsky.feed.post"]
269 },
270 "payload": {
271 "and": [
272 {"exists": ["commit", "record", "text"]},
273 {">=": [{"length": {"val": ["commit", "record", "text"]}}, 50]},
274 {"<": [{"length": {"val": ["commit", "record", "text"]}}, 300]}
275 ]
276 }
277}
278```
279
280### Condition Nodes
281
282#### Check if Like Applies to Specific DIDs
283Ensures a like event is for content from one of several specific users:
284
285```json
286{
287 "node_type": "condition",
288 "configuration": {},
289 "payload": {
290 "and": [
291 {"==": [{"val": ["commit", "collection"]}, "app.bsky.feed.like"]},
292 {"exists": ["commit", "record", "subject", "uri"]},
293 {"in": [
294 {"slice": [
295 {"split": [{"val": ["commit", "record", "subject", "uri"]}, "/"]},
296 1,
297 2
298 ]},
299 ["did:plc:user1", "did:plc:user2", "did:plc:user3"]
300 ]}
301 ]
302 }
303}
304```
305
306#### Check Post Language and Length
307Ensures posts are in English and meet length requirements:
308
309```json
310{
311 "node_type": "condition",
312 "configuration": {},
313 "payload": {
314 "and": [
315 {"exists": ["commit", "record", "text"]},
316 {">=": [{"length": {"val": ["commit", "record", "text"]}}, 10]},
317 {"<=": [{"length": {"val": ["commit", "record", "text"]}}, 280]},
318 {"or": [
319 {"in": ["en", {"val": ["commit", "record", "langs"]}]},
320 {"!": {"exists": ["commit", "record", "langs"]}}
321 ]}
322 ]
323 }
324}
325```
326
327#### Check if Event is Recent
328Ensures events are from the last 24 hours:
329
330```json
331{
332 "node_type": "condition",
333 "configuration": {},
334 "payload": {
335 ">": [
336 {"val": ["time_us"]},
337 {"/": [{"-": [{"*": [{"now": []}, 1000]}, 86400000]}, 1000]}
338 ]
339 }
340}
341```
342
343#### Validate Calendar Event Location
344Ensures calendar events are in specific cities:
345
346```json
347{
348 "node_type": "condition",
349 "configuration": {},
350 "payload": {
351 "and": [
352 {"==": [{"val": ["commit", "collection"]}, "community.lexicon.calendar.event"]},
353 {"exists": ["commit", "record", "locations"]},
354 {">": [{"length": {"val": ["commit", "record", "locations"]}}, 0]},
355 {"some": [
356 {"val": ["commit", "record", "locations"]},
357 {"in": [
358 {"val": ["locality"]},
359 ["Dayton", "Columbus", "Cincinnati", "Cleveland"]
360 ]}
361 ]}
362 ]
363 }
364}
365```
366
367### Webhook Entry Nodes
368
369Webhook entry nodes process HTTP webhook requests, allowing external systems to trigger blueprints via HTTP POST requests to `/webhooks/{blueprint_record_key}`.
370
371#### Basic Webhook Handler
372Processes any valid JSON webhook request:
373
374```json
375{
376 "node_type": "webhook_entry",
377 "configuration": {},
378 "payload": true
379}
380```
381
382#### Filter by Content-Type
383Only processes webhooks with specific content type:
384
385```json
386{
387 "node_type": "webhook_entry",
388 "configuration": {},
389 "payload": {
390 "==": [{"val": ["headers", "content-type"]}, "application/json"]
391 }
392}
393```
394
395#### Validate Webhook Structure
396Ensures webhook contains required fields:
397
398```json
399{
400 "node_type": "webhook_entry",
401 "configuration": {},
402 "payload": {
403 "and": [
404 {"exists": ["body", "event_type"]},
405 {"exists": ["body", "data"]},
406 {"in": [{"val": ["body", "event_type"]}, ["user_signup", "payment_success", "order_complete"]]}
407 ]
408 }
409}
410```
411
412**Webhook Request Structure:**
413```json
414{
415 "headers": {
416 "content-type": "application/json",
417 "authorization": "Bearer token"
418 },
419 "body": {
420 "event_type": "user_signup",
421 "data": { /* event data */ }
422 },
423 "query": { /* URL parameters */ },
424 "method": "POST",
425 "path": "/webhooks/blueprint-record-key"
426}
427```
428
429### Periodic Entry Nodes
430
431Periodic entry nodes generate events on a schedule rather than reacting to external events. They use cron expressions to define when they should trigger.
432
433#### Hourly Status Report
434Generates a status report every hour:
435
436```json
437{
438 "node_type": "periodic_entry",
439 "configuration": {
440 "cron": "0 0 * * * *" // Every hour at minute 0
441 },
442 "payload": {
443 "event_type": "hourly_status",
444 "timestamp": {"datetime": [{"now": []}]},
445 "metadata": {
446 "source": "scheduler",
447 "hour": {"format_date": [{"now": []}, "HH"]},
448 "day_of_week": {"format_date": [{"now": []}, "dddd"]}
449 }
450 }
451}
452```
453
454#### Daily Summary at 9 AM
455Creates a daily summary post every day at 9 AM:
456
457```json
458{
459 "node_type": "periodic_entry",
460 "configuration": {
461 "cron": "0 0 9 * * *" // Daily at 9:00 AM
462 },
463 "payload": {
464 "event_type": "daily_summary",
465 "timestamp": {"datetime": [{"now": []}]},
466 "summary": {
467 "date": {"format_date": [{"now": []}, "YYYY-MM-DD"]},
468 "is_weekend": {"in": [
469 {"format_date": [{"now": []}, "dddd"]},
470 ["Saturday", "Sunday"]
471 ]}
472 }
473 }
474}
475```
476
477#### Weekly Monday Morning Report
478Runs every Monday at 8 AM:
479
480```json
481{
482 "node_type": "periodic_entry",
483 "configuration": {
484 "cron": "0 0 8 * * MON" // Every Monday at 8:00 AM
485 },
486 "payload": {
487 "event_type": "weekly_report",
488 "week_number": {"format_date": [{"now": []}, "W"]},
489 "year": {"format_date": [{"now": []}, "YYYY"]},
490 "timestamp": {"datetime": [{"now": []}]}
491 }
492}
493```
494
495#### Every 30 Minutes Check
496Performs a check every 30 minutes:
497
498```json
499{
500 "node_type": "periodic_entry",
501 "configuration": {
502 "cron": "0 */30 * * * *" // Every 30 minutes
503 },
504 "payload": {
505 "event_type": "periodic_check",
506 "timestamp": {"datetime": [{"now": []}]},
507 "check_id": {"uuid": []}
508 }
509}
510```
511
512#### Business Hours Monitoring
513Active only during business hours (9 AM - 5 PM on weekdays):
514
515```json
516{
517 "node_type": "periodic_entry",
518 "configuration": {
519 "cron": "0 */15 9-17 * * MON-FRI" // Every 15 minutes, 9 AM - 5 PM, Monday-Friday
520 },
521 "payload": {
522 "event_type": "business_hours_check",
523 "timestamp": {"datetime": [{"now": []}]},
524 "is_business_hours": true
525 }
526}
527```
528
529**Important Notes for Periodic Entry:**
530- Cron expressions must have intervals between 30 seconds and 90 days
531- Use 6-field cron format for second-level precision: `SEC MIN HOUR DAY MONTH WEEKDAY`
532- Use 5-field format for minute-level precision: `MIN HOUR DAY MONTH WEEKDAY`
533- Special strings are supported: `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`
534- The payload is evaluated each time the schedule triggers, allowing dynamic content
535
536### Transform Nodes
537
538#### Create Calendar RSVP from Calendar Event
539Automatically RSVP "going" to calendar events:
540
541```json
542{
543 "node_type": "transform",
544 "configuration": {},
545 "payload": {
546 "record": {
547 "$type": "community.lexicon.calendar.rsvp",
548 "createdAt": {"datetime": [{"now": []}]},
549 "status": "community.lexicon.calendar.rsvp#going",
550 "subject": {
551 "cid": {"val": ["commit", "cid"]},
552 "uri": {"cat": [
553 "at://",
554 {"val": ["did"]},
555 "/",
556 {"val": ["commit", "collection"]},
557 "/",
558 {"val": ["commit", "rkey"]}
559 ]}
560 }
561 }
562 }
563}
564```
565
566#### Extract Post Text and Author
567Extract specific fields from a Bluesky post for further processing:
568
569```json
570{
571 "node_type": "transform",
572 "configuration": {},
573 "payload": {
574 "post_text": {"val": ["commit", "record", "text"]},
575 "author_did": {"val": ["did"]},
576 "post_uri": {"cat": [
577 "at://",
578 {"val": ["did"]},
579 "/app.bsky.feed.post/",
580 {"val": ["commit", "rkey"]}
581 ]},
582 "created_time": {"val": ["commit", "record", "createdAt"]},
583 "has_images": {"exists": ["commit", "record", "embed", "images"]},
584 "lang": {"first": {"val": ["commit", "record", "langs"]}}
585 }
586}
587```
588
589#### Create Post with Conditional Content
590Creates different post content based on the type of event:
591
592```json
593{
594 "node_type": "transform",
595 "configuration": {},
596 "payload": {
597 "record": {
598 "$type": "app.bsky.feed.post",
599 "createdAt": {"datetime": [{"now": []}]},
600 "text": {"if": [
601 {"==": [{"val": ["commit", "collection"]}, "app.bsky.feed.like"]},
602 {"cat": [
603 "Thanks for the like on my post! ❤️"
604 ]},
605 {"if": [
606 {"==": [{"val": ["commit", "collection"]}, "app.bsky.feed.repost"]},
607 "Thanks for the repost! 🔄",
608 {"cat": [
609 "New activity: ",
610 {"val": ["commit", "collection"]}
611 ]}
612 ]}
613 ]},
614 "langs": ["en"]
615 }
616 }
617}
618```
619
620#### Process Array Data
621Transform array data and calculate aggregates:
622
623```json
624{
625 "node_type": "transform",
626 "configuration": {},
627 "payload": {
628 "event_locations": {"map": [
629 {"val": ["commit", "record", "locations"]},
630 {
631 "name": {"val": ["name"]},
632 "city": {"val": ["locality"]},
633 "full_address": {"cat": [
634 {"val": ["name"]}, ", ",
635 {"val": ["locality"]}, ", ",
636 {"val": ["region"]}
637 ]}
638 }
639 ]},
640 "location_count": {"length": {"val": ["commit", "record", "locations"]}},
641 "has_remote_option": {"some": [
642 {"val": ["commit", "record", "locations"]},
643 {"==": [{"val": ["type"]}, "virtual"]}
644 ]}
645 }
646}
647```
648
649### Publish Record Nodes
650
651#### Publish Auto-Generated Like
652Automatically like posts from specific accounts:
653
654```json
655{
656 "node_type": "publish_record",
657 "configuration": {
658 "collection": "app.bsky.feed.like",
659 "did": "did:plc:your-did-here"
660 },
661 "payload": {"val": []}
662}
663```
664
665#### Publish Reply with Dynamic Content
666Create a reply with content based on the original post:
667
668```json
669{
670 "node_type": "publish_record",
671 "configuration": {
672 "collection": "app.bsky.feed.post",
673 "did": "did:plc:your-did-here"
674 },
675 "payload": {"val": []}
676}
677```
678*Note: The actual record content should come from a preceding transform node.*
679
680#### Publish Calendar RSVP
681Publish an RSVP record to a calendar event:
682
683```json
684{
685 "node_type": "publish_record",
686 "configuration": {
687 "collection": "community.lexicon.calendar.rsvp",
688 "did": "did:plc:your-did-here",
689 "rkey": {"cat": [
690 {"val": ["commit", "rkey"]},
691 "-rsvp"
692 ]}
693 },
694 "payload": {"val": []}
695}
696```
697
698### Publish Webhook Nodes
699
700#### Send to Analytics Service
701Send event data to an external analytics service:
702
703```json
704{
705 "node_type": "publish_webhook",
706 "configuration": {
707 "url": "https://analytics.example.com/events",
708 "timeout_ms": 5000,
709 "headers": {
710 "Authorization": "Bearer your-api-key",
711 "Content-Type": "application/json"
712 }
713 },
714 "payload": {"val": []}
715}
716```
717
718#### Send Alert for High-Value Posts
719Send alerts when posts exceed engagement thresholds:
720
721```json
722{
723 "node_type": "publish_webhook",
724 "configuration": {
725 "url": "https://alerts.example.com/high-engagement",
726 "timeout_ms": 10000,
727 "headers": {
728 "X-Alert-Type": "engagement",
729 "Authorization": "Bearer alert-api-key"
730 }
731 },
732 "payload": {
733 "alert_type": "high_engagement",
734 "post_uri": {"cat": [
735 "at://",
736 {"val": ["did"]},
737 "/app.bsky.feed.post/",
738 {"val": ["commit", "rkey"]}
739 ]},
740 "metrics": {
741 "likes": {"val": ["metrics", "likeCount"]},
742 "reposts": {"val": ["metrics", "repostCount"]},
743 "replies": {"val": ["metrics", "replyCount"]}
744 },
745 "timestamp": {"datetime": [{"now": []}]}
746 }
747}
748```
749
750#### Cross-Post to External Platform
751Transform and send data to external social media platforms:
752
753```json
754{
755 "node_type": "publish_webhook",
756 "configuration": {
757 "url": "https://api.external-platform.com/posts",
758 "timeout_ms": 15000,
759 "headers": {
760 "Authorization": "Bearer external-platform-token",
761 "User-Agent": "ATProto-Bridge/1.0"
762 }
763 },
764 "payload": {
765 "content": {"val": ["commit", "record", "text"]},
766 "author": {"val": ["did"]},
767 "source": "bluesky",
768 "original_uri": {"cat": [
769 "at://",
770 {"val": ["did"]},
771 "/app.bsky.feed.post/",
772 {"val": ["commit", "rkey"]}
773 ]}
774 }
775}
776```
777
778### Facet Text Nodes
779
780Facet text nodes process text to extract mentions (@handles) and URLs, creating AT Protocol facets that enable rich text features like clickable mentions and links. They're typically placed before publish_record nodes to enrich text content.
781
782#### Basic Text Processing
783Process text to extract all mentions and URLs:
784
785```json
786{
787 "node_type": "facet_text",
788 "configuration": {},
789 "payload": {}
790}
791```
792
793#### Custom Field Processing
794Process text from a specific field:
795
796```json
797{
798 "node_type": "facet_text",
799 "configuration": {
800 "field": "content"
801 },
802 "payload": {}
803}
804```
805
806#### Complete Post Creation Pipeline
807Transform data, process facets, and publish a post:
808
809```json
810{
811 "node_type": "transform",
812 "configuration": {},
813 "payload": {
814 "text": {"cat": [
815 "Thanks for following @",
816 {"resolve_handle": {"val": ["did"]}},
817 "! Check out our website: https://example.com"
818 ]}
819 }
820}
821```
822
823Followed by:
824
825```json
826{
827 "node_type": "facet_text",
828 "configuration": {
829 "field": "text"
830 },
831 "payload": {}
832}
833```
834
835Then:
836
837```json
838{
839 "node_type": "transform",
840 "configuration": {},
841 "payload": {
842 "record": {
843 "$type": "app.bsky.feed.post",
844 "text": {"val": ["text"]},
845 "facets": {"val": ["facets"]},
846 "createdAt": {"datetime": [{"now": []}]},
847 "langs": ["en"]
848 }
849 }
850}
851```
852
853#### Process Rich Content
854Handle text with multiple mentions and links:
855
856```json
857{
858 "node_type": "facet_text",
859 "configuration": {},
860 "payload": {}
861}
862```
863
864*Input data example:*
865```json
866{
867 "text": "Great discussion with @alice.bsky.social and @bob.test.com! More info at https://docs.example.com/guide and https://help.site.org"
868}
869```
870
871*Output structure:*
872```json
873{
874 "text": "Great discussion with @alice.bsky.social and @bob.test.com! More info at https://docs.example.com/guide and https://help.site.org",
875 "facets": [
876 {
877 "index": {"byteStart": 21, "byteEnd": 39},
878 "features": [{"$type": "app.bsky.richtext.facet#mention", "did": "did:plc:alice123"}]
879 },
880 {
881 "index": {"byteStart": 44, "byteEnd": 57},
882 "features": [{"$type": "app.bsky.richtext.facet#mention", "did": "did:plc:bob456"}]
883 },
884 {
885 "index": {"byteStart": 72, "byteEnd": 102},
886 "features": [{"$type": "app.bsky.richtext.facet#link", "uri": "https://docs.example.com/guide"}]
887 },
888 {
889 "index": {"byteStart": 107, "byteEnd": 127},
890 "features": [{"$type": "app.bsky.richtext.facet#link", "uri": "https://help.site.org"}]
891 }
892 ]
893}
894```
895
896**Important Notes for Facet Text:**
897- Handles are resolved to DIDs using the identity resolver
898- Unresolvable handles are skipped (rendered as plain text)
899- Both mentions and URLs are processed simultaneously
900- Byte positions are calculated accurately for UTF-8 text
901- Output includes both the original text and the facets array
902- Use before publish_record nodes that create posts with rich text
903
904### Debug Action Nodes
905
906Debug action nodes log data as it flows through the blueprint pipeline. They're essential for development, testing, and troubleshooting blueprint behavior. Debug nodes pass data through unchanged, allowing normal pipeline execution to continue.
907
908#### Basic Debug Logging
909Log data at any point in the pipeline:
910
911```json
912{
913 "node_type": "debug_action",
914 "configuration": {},
915 "payload": {}
916}
917```
918
919#### Debug After Filtering
920Inspect what data passes through conditions:
921
922```json
923{
924 "nodes": [
925 {
926 "node_type": "jetstream_entry",
927 "configuration": {
928 "collection": ["app.bsky.feed.post"]
929 },
930 "payload": true
931 },
932 {
933 "node_type": "condition",
934 "configuration": {},
935 "payload": {
936 "contains": [{"val": ["commit", "record", "text"]}, "debug"]
937 }
938 },
939 {
940 "node_type": "debug_action",
941 "configuration": {},
942 "payload": {}
943 }
944 ]
945}
946```
947
948#### Debug Transformation Results
949Verify transform node outputs:
950
951```json
952{
953 "nodes": [
954 {
955 "node_type": "webhook_entry",
956 "configuration": {},
957 "payload": true
958 },
959 {
960 "node_type": "transform",
961 "configuration": {},
962 "payload": {
963 "processed": true,
964 "original_path": {"val": ["path"]},
965 "timestamp": {"datetime": [{"now": []}]}
966 }
967 },
968 {
969 "node_type": "debug_action",
970 "configuration": {},
971 "payload": {}
972 },
973 {
974 "node_type": "publish_webhook",
975 "configuration": {
976 "url": "https://example.com/webhook"
977 },
978 "payload": {"val": []}
979 }
980 ]
981}
982```
983
984#### Multiple Debug Points
985Trace data flow through complex pipelines:
986
987```json
988{
989 "nodes": [
990 {
991 "node_type": "jetstream_entry",
992 "configuration": {
993 "collection": ["app.bsky.feed.like"]
994 },
995 "payload": true
996 },
997 {
998 "node_type": "debug_action",
999 "configuration": {},
1000 "payload": {}
1001 },
1002 {
1003 "node_type": "transform",
1004 "configuration": {},
1005 "payload": {
1006 "simplified": {"val": ["commit", "record"]}
1007 }
1008 },
1009 {
1010 "node_type": "debug_action",
1011 "configuration": {},
1012 "payload": {}
1013 }
1014 ]
1015}
1016```
1017
1018**Debug Output Format:**
1019Debug logs include:
1020- Node AT-URI
1021- Blueprint AT-URI
1022- Pretty-printed JSON of the input data
1023
1024**Use Cases for Debug Actions:**
10251. **Development**: Inspect data structure at various pipeline stages
10262. **Testing**: Verify transformations are working correctly
10273. **Troubleshooting**: Identify where data filtering or transformation issues occur
10284. **Validation**: Ensure data matches expected format before actions
10295. **Monitoring**: Log specific events for analysis
1030
1031**Tips:**
1032- Place after transforms to verify output format
1033- Place before conditions to see what data is being evaluated
1034- Place before action nodes to verify final data structure
1035- Use multiple debug nodes to trace data flow through complex pipelines
1036- Remove or comment out debug nodes in production blueprints
1037
1038## Blueprint Structure
1039
1040### Basic Blueprint Flow
1041
10421. **Entry Node**: `jetstream_entry` - Defines the trigger
10432. **Condition Node**: `condition` - Filters events (optional)
10443. **Transform Node**: `transform` - Modifies data (optional)
10454. **Action Node**: `publish_record` or `publish_webhook` - Performs action
1046
1047### Example: Auto-RSVP to Calendar Events
1048
1049```json
1050{
1051 "nodes": [
1052 {
1053 "node_type": "jetstream_entry",
1054 "configuration": {
1055 "collection": ["community.lexicon.calendar.event"]
1056 },
1057 "payload": true
1058 },
1059 {
1060 "node_type": "condition",
1061 "configuration": {},
1062 "payload": {"==": [
1063 {"val": ["commit", "record", "locations", 0, "locality"]},
1064 "Dayton"
1065 ]}
1066 },
1067 {
1068 "node_type": "transform",
1069 "configuration": {},
1070 "payload": {
1071 "record": {
1072 "$type": "community.lexicon.calendar.rsvp",
1073 "createdAt": {"datetime": [{"now": []}]},
1074 "status": "community.lexicon.calendar.rsvp#going",
1075 "subject": {
1076 "cid": {"val": ["commit", "cid"]},
1077 "uri": {"cat": [
1078 "at://",
1079 {"val": ["did"]},
1080 "/",
1081 {"val": ["commit", "collection"]},
1082 "/",
1083 {"val": ["commit", "rkey"]}
1084 ]}
1085 }
1086 }
1087 }
1088 },
1089 {
1090 "node_type": "publish_record",
1091 "configuration": {
1092 "collection": "community.lexicon.calendar.rsvp",
1093 "did": "did:plc:your-did-here"
1094 },
1095 "payload": {"val": []}
1096 }
1097 ]
1098}
1099```
1100
1101## Best Practices
1102
1103### Entry Node Configuration
1104
1105- Use specific collections when possible to reduce processing overhead
1106- Use DID filters for user-specific automation
1107- Combine multiple filters for precise targeting
1108
1109### Condition Expressions
1110
1111- Use DataLogic expressions with `{"val": [...]}` to access nested data
1112- Test conditions thoroughly with sample data
1113- Keep expressions simple and readable
1114- Use logical operators like `{"and": [...]}`, `{"or": [...]}` for complex conditions
1115- Remember conditions must evaluate to boolean values
1116
1117### Transform Templates
1118
1119- Use DataLogic expressions for dynamic content generation
1120- Include proper `$type` and `createdAt` fields for AT Protocol records
1121- Wrap record data in a `"record"` field for publish_record nodes
1122- Use `{"now": []}` and `{"datetime": [...]}` for timestamps
1123- Test templates with actual event data to verify correct field access
1124
1125### Security Considerations
1126
1127- Never hardcode sensitive information in blueprints
1128- Use environment variables for API keys and secrets
1129- Validate input data in condition nodes
1130- Be mindful of rate limits when publishing records
1131
1132## Available Node Types
1133
1134- **jetstream_entry**: Entry point for AT Protocol events from Jetstream
1135- **webhook_entry**: Entry point for HTTP webhook requests
1136- **periodic_entry**: Entry point for scheduled/cron-based execution
1137- **condition**: Filter events based on DataLogic expressions
1138- **transform**: Transform data using DataLogic expressions
1139- **facet_text**: Process text to extract mentions, hashtags, and links
1140- **publish_record**: Publish records to AT Protocol repositories
1141- **publish_webhook**: Send HTTP requests to external services
1142- **debug_action**: Log information for debugging purposes
1143
1144**Entry Nodes** (must be first node): `jetstream_entry`, `webhook_entry`, `periodic_entry`
1145**Action Nodes** (blueprint must have at least one): `publish_record`, `publish_webhook`, `debug_action`
1146
1147## Testing Blueprints
1148
11491. Use the debug_action node to log intermediate data
11502. Test with known event structures
11513. Validate generated records against Lexicon schemas
11524. Monitor logs for processing errors
11535. Start with simple blueprints and gradually add complexity
1154
1155## Complex Blueprint Examples
1156
1157### Multi-Stage Content Analysis Pipeline
1158A sophisticated blueprint that analyzes posts for sentiment, extracts mentions, and triggers different actions based on analysis results:
1159
1160```json
1161{
1162 "nodes": [
1163 {
1164 "node_type": "jetstream_entry",
1165 "configuration": {
1166 "collection": ["app.bsky.feed.post"],
1167 "did": ["did:plc:specific-user"]
1168 },
1169 "payload": true
1170 },
1171 {
1172 "node_type": "condition",
1173 "configuration": {},
1174 "payload": {"and": [
1175 {">": [{"length": {"val": ["commit", "record", "text"]}}, 10]},
1176 {"<": [{"length": {"val": ["commit", "record", "text"]}}, 300]},
1177 {"not": {"contains": [{"val": ["commit", "record", "text"]}, "RT @"]}}
1178 ]}
1179 },
1180 {
1181 "node_type": "transform",
1182 "configuration": {},
1183 "payload": {
1184 "analysis": {
1185 "post_text": {"val": ["commit", "record", "text"]},
1186 "word_count": {"length": {"split": [{"val": ["commit", "record", "text"]}, " "]}},
1187 "has_mentions": {"some": [
1188 {"extract_mentions": {"val": ["commit", "record", "text"]}},
1189 true
1190 ]},
1191 "mention_count": {"length": {"extract_mentions": {"val": ["commit", "record", "text"]}}},
1192 "has_hashtags": {"contains": [{"val": ["commit", "record", "text"]}, "#"]},
1193 "languages": {"val": ["commit", "record", "langs"]},
1194 "engagement_score": {"+": [
1195 {"*": [{"val": ["metrics", "likeCount"]}, 1]},
1196 {"*": [{"val": ["metrics", "repostCount"]}, 2]},
1197 {"*": [{"val": ["metrics", "replyCount"]}, 3]}
1198 ]},
1199 "priority_level": {"if": [
1200 {">": [{"val": ["metrics", "likeCount"]}, 100]},
1201 "high",
1202 {"if": [
1203 {">": [{"val": ["metrics", "likeCount"]}, 10]},
1204 "medium",
1205 "low"
1206 ]}
1207 ]}
1208 },
1209 "metadata": {
1210 "processed_at": {"datetime": [{"now": []}]},
1211 "post_uri": {"cat": [
1212 "at://",
1213 {"val": ["did"]},
1214 "/app.bsky.feed.post/",
1215 {"val": ["commit", "rkey"]}
1216 ]},
1217 "author_handle": {"val": ["handle"]}
1218 }
1219 }
1220 },
1221 {
1222 "node_type": "condition",
1223 "configuration": {},
1224 "payload": {"==": [{"val": ["analysis", "priority_level"]}, "high"]}
1225 },
1226 {
1227 "node_type": "publish_webhook",
1228 "configuration": {
1229 "url": "https://api.analytics.example.com/high-priority-posts",
1230 "timeout_ms": 5000,
1231 "headers": {
1232 "Authorization": "Bearer analytics-key",
1233 "X-Priority": "high"
1234 }
1235 },
1236 "payload": {"val": []}
1237 }
1238 ]
1239}
1240```
1241
1242### Dynamic Event Aggregation with Conditional Responses
1243A blueprint that aggregates events by type and responds differently based on patterns:
1244
1245```json
1246{
1247 "nodes": [
1248 {
1249 "node_type": "jetstream_entry",
1250 "configuration": {
1251 "collection": [
1252 "app.bsky.feed.like",
1253 "app.bsky.feed.repost",
1254 "app.bsky.graph.follow"
1255 ]
1256 },
1257 "payload": true
1258 },
1259 {
1260 "node_type": "transform",
1261 "configuration": {},
1262 "payload": {
1263 "event_summary": {
1264 "type": {"val": ["commit", "collection"]},
1265 "actor": {"val": ["did"]},
1266 "target": {"if": [
1267 {"==": [{"val": ["commit", "collection"]}, "app.bsky.feed.like"]},
1268 {"val": ["commit", "record", "subject", "uri"]},
1269 {"if": [
1270 {"==": [{"val": ["commit", "collection"]}, "app.bsky.feed.repost"]},
1271 {"val": ["commit", "record", "subject", "uri"]},
1272 {"val": ["commit", "record", "subject"]}
1273 ]}
1274 ]},
1275 "timestamp": {"datetime": [{"now": []}]}
1276 },
1277 "aggregation_key": {"cat": [
1278 {"val": ["did"]},
1279 "-",
1280 {"date_trunc": ["hour", {"now": []}]}
1281 ]},
1282 "response_action": {"if": [
1283 {"==": [{"val": ["commit", "collection"]}, "app.bsky.graph.follow"]},
1284 "send_welcome",
1285 {"if": [
1286 {"and": [
1287 {"==": [{"val": ["commit", "collection"]}, "app.bsky.feed.like"]},
1288 {">=": [{"count": {"val": ["hourly_likes"]}}, 10]}
1289 ]},
1290 "send_thanks",
1291 "log_only"
1292 ]}
1293 ]}
1294 }
1295 },
1296 {
1297 "node_type": "condition",
1298 "configuration": {},
1299 "payload": {"!=": [{"val": ["response_action"]}, "log_only"]}
1300 },
1301 {
1302 "node_type": "transform",
1303 "configuration": {},
1304 "payload": {
1305 "record": {
1306 "$type": "app.bsky.feed.post",
1307 "createdAt": {"datetime": [{"now": []}]},
1308 "text": {"if": [
1309 {"==": [{"val": ["response_action"]}, "send_welcome"]},
1310 {"cat": [
1311 "Welcome to the community, ",
1312 {"resolve_handle": {"val": ["event_summary", "actor"]}},
1313 "! Thanks for the follow! 🎉"
1314 ]},
1315 {"cat": [
1316 "Thanks for all the engagement today, ",
1317 {"resolve_handle": {"val": ["event_summary", "actor"]}},
1318 "! Your support means a lot! ❤️"
1319 ]}
1320 ]},
1321 "langs": ["en"]
1322 }
1323 }
1324 },
1325 {
1326 "node_type": "publish_record",
1327 "configuration": {
1328 "collection": "app.bsky.feed.post",
1329 "did": "did:plc:your-did-here"
1330 },
1331 "payload": {"val": []}
1332 }
1333 ]
1334}
1335```
1336
1337### Advanced Data Processing with Error Handling
1338A blueprint that processes complex nested data structures with fallback logic:
1339
1340```json
1341{
1342 "nodes": [
1343 {
1344 "node_type": "jetstream_entry",
1345 "configuration": {
1346 "collection": ["community.lexicon.calendar.event"]
1347 },
1348 "payload": true
1349 },
1350 {
1351 "node_type": "transform",
1352 "configuration": {},
1353 "payload": {
1354 "processed_event": {
1355 "id": {"val": ["commit", "rkey"]},
1356 "name": {"or": [
1357 {"val": ["commit", "record", "name"]},
1358 "Untitled Event"
1359 ]},
1360 "description": {"or": [
1361 {"val": ["commit", "record", "description"]},
1362 ""
1363 ]},
1364 "start_time": {"or": [
1365 {"val": ["commit", "record", "startTime"]},
1366 {"datetime": [{"now": []}]}
1367 ]},
1368 "locations": {"map": [
1369 {"or": [
1370 {"val": ["commit", "record", "locations"]},
1371 []
1372 ]},
1373 {
1374 "name": {"or": [{"val": ["name"]}, "Unknown Location"]},
1375 "address": {"if": [
1376 {"exists": ["address"]},
1377 {"cat": [
1378 {"or": [{"val": ["address", "streetAddress"]}, ""]},
1379 {"if": [
1380 {"and": [
1381 {"exists": ["address", "streetAddress"]},
1382 {"exists": ["address", "locality"]}
1383 ]},
1384 ", ",
1385 ""
1386 ]},
1387 {"or": [{"val": ["address", "locality"]}, ""]},
1388 {"if": [
1389 {"and": [
1390 {"exists": ["address", "locality"]},
1391 {"exists": ["address", "region"]}
1392 ]},
1393 ", ",
1394 ""
1395 ]},
1396 {"or": [{"val": ["address", "region"]}, ""]}
1397 ]},
1398 "Address not provided"
1399 ]},
1400 "coordinates": {"if": [
1401 {"and": [
1402 {"exists": ["geo", "latitude"]},
1403 {"exists": ["geo", "longitude"]}
1404 ]},
1405 {
1406 "lat": {"val": ["geo", "latitude"]},
1407 "lng": {"val": ["geo", "longitude"]}
1408 },
1409 null
1410 ]}
1411 }
1412 ]},
1413 "attendee_capacity": {"or": [
1414 {"val": ["commit", "record", "capacity"]},
1415 -1
1416 ]},
1417 "is_virtual": {"some": [
1418 {"or": [
1419 {"val": ["commit", "record", "locations"]},
1420 []
1421 ]},
1422 {"==": [{"val": ["type"]}, "virtual"]}
1423 ]},
1424 "tags": {"filter": [
1425 {"extract_hashtags": {"or": [
1426 {"val": ["commit", "record", "description"]},
1427 ""
1428 ]}},
1429 {"!=": [{"val": []}, ""]}
1430 ]}
1431 }
1432 }
1433 },
1434 {
1435 "node_type": "condition",
1436 "configuration": {},
1437 "payload": {"and": [
1438 {"!=": [{"val": ["processed_event", "name"]}, "Untitled Event"]},
1439 {">": [{"length": {"val": ["processed_event", "locations"]}}, 0]}
1440 ]}
1441 },
1442 {
1443 "node_type": "publish_webhook",
1444 "configuration": {
1445 "url": "https://api.eventprocessor.example.com/events",
1446 "timeout_ms": 10000,
1447 "headers": {
1448 "Content-Type": "application/json",
1449 "X-Processor-Version": "2.0"
1450 }
1451 },
1452 "payload": {"val": []}
1453 }
1454 ]
1455}
1456```
1457
1458## Common Patterns
1459
1460### Auto-Reply to Mentions
1461```json
1462{
1463 "entry": "jetstream_entry with mention detection",
1464 "condition": "check if mentioned user matches yours",
1465 "action": "publish_record with reply"
1466}
1467```
1468
1469### Content Moderation
1470```json
1471{
1472 "entry": "jetstream_entry for posts",
1473 "condition": "check for inappropriate content",
1474 "action": "publish_webhook to moderation service"
1475}
1476```
1477
1478### Cross-Platform Posting
1479```json
1480{
1481 "entry": "jetstream_entry for your posts",
1482 "transform": "convert to external platform format",
1483 "action": "publish_webhook to external API"
1484}
1485```
1486
1487### Event Aggregation
1488```json
1489{
1490 "entry": "jetstream_entry for specific collection",
1491 "condition": "filter relevant events",
1492 "action": "publish_webhook to analytics service"
1493}
1494```
1495
1496### Scheduled Daily Post
1497Create a daily motivational post at 9 AM:
1498```json
1499{
1500 "nodes": [
1501 {
1502 "node_type": "periodic_entry",
1503 "configuration": {
1504 "cron": "0 0 9 * * *" // Daily at 9 AM
1505 },
1506 "payload": {
1507 "event_type": "daily_motivation",
1508 "day": {"format_date": [{"now": []}, "dddd"]},
1509 "timestamp": {"datetime": [{"now": []}]}
1510 }
1511 },
1512 {
1513 "node_type": "transform",
1514 "configuration": {},
1515 "payload": {
1516 "record": {
1517 "$type": "app.bsky.feed.post",
1518 "createdAt": {"datetime": [{"now": []}]},
1519 "text": {"if": [
1520 {"==": [{"val": ["day"]}, "Monday"]},
1521 "Happy Monday! 💪 Let's make this week amazing!",
1522 {"if": [
1523 {"==": [{"val": ["day"]}, "Friday"]},
1524 "It's Friday! 🎉 Almost weekend time!",
1525 {"cat": [
1526 "Good morning! Today is ",
1527 {"val": ["day"]},
1528 " - make it count! ☀️"
1529 ]}
1530 ]}
1531 ]},
1532 "langs": ["en"]
1533 }
1534 }
1535 },
1536 {
1537 "node_type": "publish_record",
1538 "configuration": {
1539 "collection": "app.bsky.feed.post",
1540 "did": "did:plc:your-did-here"
1541 },
1542 "payload": {"val": []}
1543 }
1544 ]
1545}
1546```